Merge "Fix issue with floating tasks being tracked as a drag running task" into main
diff --git a/core/api/current.txt b/core/api/current.txt
index c0b6ab6..9fc350d 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -19373,6 +19373,7 @@
     field @FlaggedApi("com.android.internal.camera.flags.color_temperature") @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<android.util.Range<java.lang.Integer>> COLOR_CORRECTION_COLOR_TEMPERATURE_RANGE;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> CONTROL_AE_AVAILABLE_ANTIBANDING_MODES;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> CONTROL_AE_AVAILABLE_MODES;
+    field @FlaggedApi("com.android.internal.camera.flags.ae_priority") @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> CONTROL_AE_AVAILABLE_PRIORITY_MODES;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<android.util.Range<java.lang.Integer>[]> CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<android.util.Range<java.lang.Integer>> CONTROL_AE_COMPENSATION_RANGE;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<android.util.Rational> CONTROL_AE_COMPENSATION_STEP;
@@ -19690,6 +19691,9 @@
     field public static final int CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL = 2; // 0x2
     field public static final int CONTROL_AE_PRECAPTURE_TRIGGER_IDLE = 0; // 0x0
     field public static final int CONTROL_AE_PRECAPTURE_TRIGGER_START = 1; // 0x1
+    field @FlaggedApi("com.android.internal.camera.flags.ae_priority") public static final int CONTROL_AE_PRIORITY_MODE_OFF = 0; // 0x0
+    field @FlaggedApi("com.android.internal.camera.flags.ae_priority") public static final int CONTROL_AE_PRIORITY_MODE_SENSOR_EXPOSURE_TIME_PRIORITY = 2; // 0x2
+    field @FlaggedApi("com.android.internal.camera.flags.ae_priority") public static final int CONTROL_AE_PRIORITY_MODE_SENSOR_SENSITIVITY_PRIORITY = 1; // 0x1
     field public static final int CONTROL_AE_STATE_CONVERGED = 2; // 0x2
     field public static final int CONTROL_AE_STATE_FLASH_REQUIRED = 4; // 0x4
     field public static final int CONTROL_AE_STATE_INACTIVE = 0; // 0x0
@@ -19783,6 +19787,8 @@
     field public static final int CONTROL_VIDEO_STABILIZATION_MODE_OFF = 0; // 0x0
     field public static final int CONTROL_VIDEO_STABILIZATION_MODE_ON = 1; // 0x1
     field public static final int CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION = 2; // 0x2
+    field @FlaggedApi("com.android.internal.camera.flags.zoom_method") public static final int CONTROL_ZOOM_METHOD_AUTO = 0; // 0x0
+    field @FlaggedApi("com.android.internal.camera.flags.zoom_method") public static final int CONTROL_ZOOM_METHOD_ZOOM_RATIO = 1; // 0x1
     field public static final int DISTORTION_CORRECTION_MODE_FAST = 1; // 0x1
     field public static final int DISTORTION_CORRECTION_MODE_HIGH_QUALITY = 2; // 0x2
     field public static final int DISTORTION_CORRECTION_MODE_OFF = 0; // 0x0
@@ -19972,6 +19978,7 @@
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Boolean> CONTROL_AE_LOCK;
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_AE_MODE;
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_AE_PRECAPTURE_TRIGGER;
+    field @FlaggedApi("com.android.internal.camera.flags.ae_priority") @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_AE_PRIORITY_MODE;
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<android.hardware.camera2.params.MeteringRectangle[]> CONTROL_AE_REGIONS;
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<android.util.Range<java.lang.Integer>> CONTROL_AE_TARGET_FPS_RANGE;
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_AF_MODE;
@@ -19990,6 +19997,7 @@
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_SCENE_MODE;
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_SETTINGS_OVERRIDE;
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_VIDEO_STABILIZATION_MODE;
+    field @FlaggedApi("com.android.internal.camera.flags.zoom_method") @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> CONTROL_ZOOM_METHOD;
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Float> CONTROL_ZOOM_RATIO;
     field @NonNull public static final android.os.Parcelable.Creator<android.hardware.camera2.CaptureRequest> CREATOR;
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> DISTORTION_CORRECTION_MODE;
@@ -20064,6 +20072,7 @@
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Boolean> CONTROL_AE_LOCK;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_AE_MODE;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_AE_PRECAPTURE_TRIGGER;
+    field @FlaggedApi("com.android.internal.camera.flags.ae_priority") @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_AE_PRIORITY_MODE;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<android.hardware.camera2.params.MeteringRectangle[]> CONTROL_AE_REGIONS;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_AE_STATE;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<android.util.Range<java.lang.Integer>> CONTROL_AE_TARGET_FPS_RANGE;
@@ -20088,6 +20097,7 @@
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_SCENE_MODE;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_SETTINGS_OVERRIDE;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_VIDEO_STABILIZATION_MODE;
+    field @FlaggedApi("com.android.internal.camera.flags.zoom_method") @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> CONTROL_ZOOM_METHOD;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Float> CONTROL_ZOOM_RATIO;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> DISTORTION_CORRECTION_MODE;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> EDGE_MODE;
@@ -20910,6 +20920,8 @@
     method public boolean hasPermission(android.hardware.usb.UsbDevice);
     method public boolean hasPermission(android.hardware.usb.UsbAccessory);
     method public android.os.ParcelFileDescriptor openAccessory(android.hardware.usb.UsbAccessory);
+    method @FlaggedApi("android.hardware.usb.flags.enable_accessory_stream_api") @NonNull public java.io.InputStream openAccessoryInputStream(@NonNull android.hardware.usb.UsbAccessory);
+    method @FlaggedApi("android.hardware.usb.flags.enable_accessory_stream_api") @NonNull public java.io.OutputStream openAccessoryOutputStream(@NonNull android.hardware.usb.UsbAccessory);
     method public android.hardware.usb.UsbDeviceConnection openDevice(android.hardware.usb.UsbDevice);
     method public void requestPermission(android.hardware.usb.UsbDevice, android.app.PendingIntent);
     method public void requestPermission(android.hardware.usb.UsbAccessory, android.app.PendingIntent);
@@ -21012,6 +21024,7 @@
     method @Deprecated public android.inputmethodservice.AbstractInputMethodService.AbstractInputMethodSessionImpl onCreateInputMethodSessionInterface();
     method public android.view.View onCreateInputView();
     method protected void onCurrentInputMethodSubtypeChanged(android.view.inputmethod.InputMethodSubtype);
+    method @FlaggedApi("android.view.inputmethod.ime_switcher_revamp_api") public void onCustomImeSwitcherButtonRequestedVisible(boolean);
     method public void onDisplayCompletions(android.view.inputmethod.CompletionInfo[]);
     method public boolean onEvaluateFullscreenMode();
     method @CallSuper public boolean onEvaluateInputViewShown();
@@ -22899,6 +22912,7 @@
     method public void onCryptoError(@NonNull android.media.MediaCodec, @NonNull android.media.MediaCodec.CryptoException);
     method public abstract void onError(@NonNull android.media.MediaCodec, @NonNull android.media.MediaCodec.CodecException);
     method public abstract void onInputBufferAvailable(@NonNull android.media.MediaCodec, int);
+    method @FlaggedApi("android.media.codec.subsession_metrics") public void onMetricsFlushed(@NonNull android.media.MediaCodec, @NonNull android.os.PersistableBundle);
     method public abstract void onOutputBufferAvailable(@NonNull android.media.MediaCodec, int, @NonNull android.media.MediaCodec.BufferInfo);
     method @FlaggedApi("com.android.media.codec.flags.large_audio_frame") public void onOutputBuffersAvailable(@NonNull android.media.MediaCodec, int, @NonNull java.util.ArrayDeque<android.media.MediaCodec.BufferInfo>);
     method public abstract void onOutputFormatChanged(@NonNull android.media.MediaCodec, @NonNull android.media.MediaFormat);
@@ -42159,6 +42173,25 @@
     method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setWriteSensitivity(int);
   }
 
+  @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public abstract class SettingsPreferenceService extends android.app.Service {
+    ctor public SettingsPreferenceService();
+    method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent);
+    method public abstract void onGetAllPreferenceMetadata(@NonNull android.service.settings.preferences.MetadataRequest, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.MetadataResult,java.lang.Exception>);
+    method public abstract void onGetPreferenceValue(@NonNull android.service.settings.preferences.GetValueRequest, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.GetValueResult,java.lang.Exception>);
+    method public abstract void onSetPreferenceValue(@NonNull android.service.settings.preferences.SetValueRequest, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.SetValueResult,java.lang.Exception>);
+    field public static final String ACTION_PREFERENCE_SERVICE = "android.service.settings.preferences.action.PREFERENCE_SERVICE";
+  }
+
+  @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public class SettingsPreferenceServiceClient implements java.lang.AutoCloseable {
+    ctor public SettingsPreferenceServiceClient(@NonNull android.content.Context, @NonNull String);
+    method public void close();
+    method public void getAllPreferenceMetadata(@NonNull android.service.settings.preferences.MetadataRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.MetadataResult,java.lang.Exception>);
+    method public void getPreferenceValue(@NonNull android.service.settings.preferences.GetValueRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.GetValueResult,java.lang.Exception>);
+    method public void setPreferenceValue(@NonNull android.service.settings.preferences.SetValueRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.service.settings.preferences.SetValueResult,java.lang.Exception>);
+    method public void start();
+    method public void stop();
+  }
+
   @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public final class SettingsPreferenceValue implements android.os.Parcelable {
     method public int describeContents();
     method public boolean getBooleanValue();
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 8954f8e..0e521c6 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -46,6 +46,7 @@
     field public static final String ASSOCIATE_COMPANION_DEVICES = "android.permission.ASSOCIATE_COMPANION_DEVICES";
     field public static final String BACKGROUND_CAMERA = "android.permission.BACKGROUND_CAMERA";
     field public static final String BACKUP = "android.permission.BACKUP";
+    field @FlaggedApi("android.permission.flags.health_connect_backup_restore_permission_enabled") public static final String BACKUP_HEALTH_CONNECT_DATA_AND_SETTINGS = "android.permission.BACKUP_HEALTH_CONNECT_DATA_AND_SETTINGS";
     field public static final String BATTERY_PREDICTION = "android.permission.BATTERY_PREDICTION";
     field public static final String BIND_AMBIENT_CONTEXT_DETECTION_SERVICE = "android.permission.BIND_AMBIENT_CONTEXT_DETECTION_SERVICE";
     field public static final String BIND_ATTENTION_SERVICE = "android.permission.BIND_ATTENTION_SERVICE";
@@ -347,6 +348,7 @@
     field public static final String REQUEST_NOTIFICATION_ASSISTANT_SERVICE = "android.permission.REQUEST_NOTIFICATION_ASSISTANT_SERVICE";
     field public static final String RESET_PASSWORD = "android.permission.RESET_PASSWORD";
     field public static final String RESTART_WIFI_SUBSYSTEM = "android.permission.RESTART_WIFI_SUBSYSTEM";
+    field @FlaggedApi("android.permission.flags.health_connect_backup_restore_permission_enabled") public static final String RESTORE_HEALTH_CONNECT_DATA_AND_SETTINGS = "android.permission.RESTORE_HEALTH_CONNECT_DATA_AND_SETTINGS";
     field public static final String RESTORE_RUNTIME_PERMISSIONS = "android.permission.RESTORE_RUNTIME_PERMISSIONS";
     field public static final String RESTRICTED_VR_ACCESS = "android.permission.RESTRICTED_VR_ACCESS";
     field public static final String RETRIEVE_WINDOW_CONTENT = "android.permission.RETRIEVE_WINDOW_CONTENT";
@@ -1270,13 +1272,21 @@
 
   public class WallpaperManager {
     method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public void clearWallpaper(int, int);
+    method @FlaggedApi("android.app.customization_packs_apis") @NonNull @RequiresPermission(android.Manifest.permission.READ_WALLPAPER_INTERNAL) public android.util.SparseArray<android.graphics.Rect> getBitmapCrops(int);
+    method @FlaggedApi("android.app.customization_packs_apis") public static int getOrientation(@NonNull android.graphics.Point);
     method @FloatRange(from=0.0f, to=1.0f) @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_DIM_AMOUNT) public float getWallpaperDimAmount();
+    method @FlaggedApi("android.app.customization_packs_apis") @Nullable public android.os.ParcelFileDescriptor getWallpaperFile(int, boolean);
     method @FlaggedApi("android.app.live_wallpaper_content_handling") @Nullable @RequiresPermission(android.Manifest.permission.READ_WALLPAPER_INTERNAL) public android.app.wallpaper.WallpaperInstance getWallpaperInstance(int);
     method public void setDisplayOffset(android.os.IBinder, int, int);
+    method @FlaggedApi("com.android.window.flags.multi_crop") @RequiresPermission(android.Manifest.permission.SET_WALLPAPER) public int setStreamWithCrops(@NonNull java.io.InputStream, @NonNull android.util.SparseArray<android.graphics.Rect>, boolean, int) throws java.io.IOException;
     method @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_COMPONENT) public boolean setWallpaperComponent(android.content.ComponentName);
     method @FlaggedApi("android.app.live_wallpaper_content_handling") @RequiresPermission(allOf={android.Manifest.permission.SET_WALLPAPER_COMPONENT, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}, conditional=true) public boolean setWallpaperComponentWithDescription(@NonNull android.app.wallpaper.WallpaperDescription, int);
     method @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_COMPONENT) public boolean setWallpaperComponentWithFlags(@NonNull android.content.ComponentName, int);
     method @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_DIM_AMOUNT) public void setWallpaperDimAmount(@FloatRange(from=0.0f, to=1.0f) float);
+    field @FlaggedApi("android.app.customization_packs_apis") public static final int ORIENTATION_LANDSCAPE = 1; // 0x1
+    field @FlaggedApi("android.app.customization_packs_apis") public static final int ORIENTATION_PORTRAIT = 0; // 0x0
+    field @FlaggedApi("android.app.customization_packs_apis") public static final int ORIENTATION_SQUARE_LANDSCAPE = 3; // 0x3
+    field @FlaggedApi("android.app.customization_packs_apis") public static final int ORIENTATION_SQUARE_PORTRAIT = 2; // 0x2
   }
 
 }
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 8dd12172..c8ecfa9 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -535,9 +535,13 @@
   public class WallpaperManager {
     method @Nullable public android.graphics.Bitmap getBitmap();
     method @Nullable public android.graphics.Bitmap getBitmapAsUser(int, boolean, int);
+    method @FlaggedApi("com.android.window.flags.multi_crop") @NonNull @RequiresPermission(android.Manifest.permission.READ_WALLPAPER_INTERNAL) public java.util.List<android.graphics.Rect> getBitmapCrops(@NonNull java.util.List<android.graphics.Point>, int, boolean);
+    method @FlaggedApi("com.android.window.flags.multi_crop") @NonNull public java.util.List<android.graphics.Rect> getBitmapCrops(@NonNull android.graphics.Point, @NonNull java.util.List<android.graphics.Point>, @Nullable java.util.Map<android.graphics.Point,android.graphics.Rect>);
     method public boolean isLockscreenLiveWallpaperEnabled();
     method @Nullable public android.graphics.Rect peekBitmapDimensions();
     method @Nullable public android.graphics.Rect peekBitmapDimensions(int);
+    method @FlaggedApi("com.android.window.flags.multi_crop") @RequiresPermission(android.Manifest.permission.SET_WALLPAPER) public int setBitmapWithCrops(@Nullable android.graphics.Bitmap, @NonNull java.util.Map<android.graphics.Point,android.graphics.Rect>, boolean, int) throws java.io.IOException;
+    method @FlaggedApi("com.android.window.flags.multi_crop") @RequiresPermission(android.Manifest.permission.SET_WALLPAPER) public int setStreamWithCrops(@NonNull java.io.InputStream, @NonNull java.util.Map<android.graphics.Point,android.graphics.Rect>, boolean, int) throws java.io.IOException;
     method public void setWallpaperZoomOut(@NonNull android.os.IBinder, float);
     method public boolean shouldEnableWideColorGamut();
     method public boolean wallpaperSupportsWcg(int);
@@ -3247,6 +3251,14 @@
 
 }
 
+package android.service.settings.preferences {
+
+  @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public class SettingsPreferenceServiceClient implements java.lang.AutoCloseable {
+    ctor public SettingsPreferenceServiceClient(@NonNull android.content.Context, @NonNull String, boolean, @Nullable android.content.ServiceConnection);
+  }
+
+}
+
 package android.service.voice {
 
   public class AlwaysOnHotwordDetector implements android.service.voice.HotwordDetector {
diff --git a/core/java/android/app/DisabledWallpaperManager.java b/core/java/android/app/DisabledWallpaperManager.java
index b06fb9e..233dc75 100644
--- a/core/java/android/app/DisabledWallpaperManager.java
+++ b/core/java/android/app/DisabledWallpaperManager.java
@@ -177,6 +177,13 @@
     }
 
     @Override
+    @NonNull
+    public SparseArray<Rect> getBitmapCrops(int which) {
+        unsupported();
+        return new SparseArray<>();
+    }
+
+    @Override
     public List<Rect> getBitmapCrops(@NonNull Point bitmapSize, @NonNull List<Point> displaySizes,
             @Nullable Map<Point, Rect> cropHints) {
         return unsupported();
@@ -358,8 +365,9 @@
 
 
     @Override
-    public int setStreamWithCrops(InputStream bitmapData, @NonNull SparseArray<Rect> cropHints,
-            boolean allowBackup, @SetWallpaperFlags int which) throws IOException {
+    public int setStreamWithCrops(@NonNull InputStream bitmapData,
+            @NonNull SparseArray<Rect> cropHints, boolean allowBackup, @SetWallpaperFlags int which
+    ) throws IOException {
         return unsupportedInt();
     }
 
diff --git a/core/java/android/app/IWallpaperManager.aidl b/core/java/android/app/IWallpaperManager.aidl
index f693e9b..6449ea1 100644
--- a/core/java/android/app/IWallpaperManager.aidl
+++ b/core/java/android/app/IWallpaperManager.aidl
@@ -97,6 +97,16 @@
     List getBitmapCrops(in List<Point> displaySizes, int which, boolean originalBitmap, int userId);
 
     /**
+     * For a given user, if the wallpaper of the specified which is an ImageWallpaper, return
+     * a bundle which is a Map<Integer, Rect> containing the custom cropHints that were sent to
+     * setBitmapWithCrops or setStreamWithCrops. These crops are relative to the original bitmap.
+     * If the wallpaper isn't an ImageWallpaper, return null.
+     */
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.READ_WALLPAPER_INTERNAL)")
+    @SuppressWarnings(value={"untyped-collection"})
+    Bundle getCurrentBitmapCrops(int which, int userId);
+
+    /**
      * Return how a bitmap of a given size would be cropped for a given list of display sizes when
      * set with the given suggested crops.
      * @hide
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index 479f3df..abb2dd4 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -19,9 +19,10 @@
 import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE;
 import static android.Manifest.permission.READ_WALLPAPER_INTERNAL;
 import static android.Manifest.permission.SET_WALLPAPER_DIM_AMOUNT;
+import static android.app.Flags.FLAG_CUSTOMIZATION_PACKS_APIS;
+import static android.app.Flags.FLAG_LIVE_WALLPAPER_CONTENT_HANDLING;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
-import static android.app.Flags.FLAG_LIVE_WALLPAPER_CONTENT_HANDLING;
 
 import static com.android.window.flags.Flags.FLAG_MULTI_CROP;
 import static com.android.window.flags.Flags.multiCrop;
@@ -342,24 +343,32 @@
      * Portrait orientation of most screens
      * @hide
      */
+    @FlaggedApi(FLAG_CUSTOMIZATION_PACKS_APIS)
+    @SystemApi
     public static final int ORIENTATION_PORTRAIT = 0;
 
     /**
      * Landscape orientation of most screens
      * @hide
      */
+    @FlaggedApi(FLAG_CUSTOMIZATION_PACKS_APIS)
+    @SystemApi
     public static final int ORIENTATION_LANDSCAPE = 1;
 
     /**
      * Portrait orientation with similar width and height (e.g. the inner screen of a foldable)
      * @hide
      */
+    @FlaggedApi(FLAG_CUSTOMIZATION_PACKS_APIS)
+    @SystemApi
     public static final int ORIENTATION_SQUARE_PORTRAIT = 2;
 
     /**
      * Landscape orientation with similar width and height (e.g. the inner screen of a foldable)
      * @hide
      */
+    @FlaggedApi(FLAG_CUSTOMIZATION_PACKS_APIS)
+    @SystemApi
     public static final int ORIENTATION_SQUARE_LANDSCAPE = 3;
 
     /**
@@ -368,7 +377,9 @@
      * @return the corresponding {@link ScreenOrientation}.
      * @hide
      */
-    public static @ScreenOrientation int getOrientation(Point screenSize) {
+    @FlaggedApi(FLAG_CUSTOMIZATION_PACKS_APIS)
+    @SystemApi
+    public static @ScreenOrientation int getOrientation(@NonNull Point screenSize) {
         float ratio = ((float) screenSize.x) / screenSize.y;
         // ratios between 3/4 and 4/3 are considered square
         return ratio >= 4 / 3f ? ORIENTATION_LANDSCAPE
@@ -1623,14 +1634,15 @@
      *                   If false, return areas relative to the cropped bitmap.
      * @return A List of Rect where the Rect is within the cropped/original bitmap, and corresponds
      *          to what is displayed. The Rect may have a larger width/height ratio than the screen
-     *          due to parallax. Return {@code null} if the wallpaper is not an ImageWallpaper.
-     *          Also return {@code null} when called with which={@link #FLAG_LOCK} if there is a
+     *          due to parallax. Return an empty list if the wallpaper is not an ImageWallpaper.
+     *          Also return an empty list when called with which={@link #FLAG_LOCK} if there is a
      *          shared home + lock wallpaper.
      * @hide
      */
     @FlaggedApi(FLAG_MULTI_CROP)
+    @TestApi
     @RequiresPermission(READ_WALLPAPER_INTERNAL)
-    @Nullable
+    @NonNull
     public List<Rect> getBitmapCrops(@NonNull List<Point> displaySizes,
             @SetWallpaperFlags int which, boolean originalBitmap) {
         checkExactlyOneWallpaperFlagSet(which);
@@ -1653,6 +1665,52 @@
     }
 
     /**
+     * For the current user, if the wallpaper of the specified destination is an ImageWallpaper,
+     * return the custom crops of the wallpaper, that have been provided for example via
+     * {@link #setStreamWithCrops}. These crops are relative to the original bitmap.
+     * <p>
+     * This method helps apps that change wallpapers provide an undo option. Calling
+     * {@link #setStreamWithCrops(InputStream, SparseArray, boolean, int)} with this SparseArray and
+     * the current original bitmap file, that can be obtained with {@link #getWallpaperFile(int,
+     * boolean)} with {@code getCropped=false}, will exactly lead to the current wallpaper state.
+     *
+     * @param which wallpaper type. Must be either {@link #FLAG_SYSTEM} or {@link #FLAG_LOCK}.
+     * @return A map from {{@link #ORIENTATION_PORTRAIT}, {@link #ORIENTATION_LANDSCAPE},
+     *          {@link #ORIENTATION_SQUARE_PORTRAIT}, {{@link #ORIENTATION_SQUARE_LANDSCAPE}}} to
+     *          Rect, representing the custom cropHints. The map can be empty and will only contains
+     *          entries for screen orientations for which a custom crop was provided. If no custom
+     *          crop is provided for an orientation, the system will infer the crop based on the
+     *          custom crops of the other orientations; or center-align the full image if no custom
+     *          crops are provided at all.
+     *          <p>
+     *          Return an empty map if the wallpaper is not an ImageWallpaper. Also return
+     *          an empty map when called with which={@link #FLAG_LOCK} if there is a shared
+     *          home + lock wallpaper.
+     *
+     * @hide
+     */
+    @FlaggedApi(FLAG_CUSTOMIZATION_PACKS_APIS)
+    @SystemApi
+    @RequiresPermission(READ_WALLPAPER_INTERNAL)
+    @NonNull
+    public SparseArray<Rect> getBitmapCrops(@SetWallpaperFlags int which) {
+        checkExactlyOneWallpaperFlagSet(which);
+        try {
+            Bundle bundle = sGlobals.mService.getCurrentBitmapCrops(which, mContext.getUserId());
+            SparseArray<Rect> result = new SparseArray<>();
+            if (bundle == null) return result;
+            for (String key : bundle.keySet()) {
+                int intKey = Integer.parseInt(key);
+                Rect rect = bundle.getParcelable(key, Rect.class);
+                result.put(intKey, rect);
+            }
+            return result;
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * For preview purposes.
      * Return how a bitmap of a given size would be cropped for a given list of display sizes, if
      * it was set as wallpaper via {@link #setBitmapWithCrops(Bitmap, Map, boolean, int)} or
@@ -1664,7 +1722,8 @@
      * @hide
      */
     @FlaggedApi(FLAG_MULTI_CROP)
-    @Nullable
+    @TestApi
+    @NonNull
     public List<Rect> getBitmapCrops(@NonNull Point bitmapSize, @NonNull List<Point> displaySizes,
             @Nullable Map<Point, Rect> cropHints) {
         try {
@@ -1890,9 +1949,14 @@
      *    defined kind of wallpaper, either {@link #FLAG_SYSTEM} or {@link #FLAG_LOCK}.
      * @param getCropped If true the cropped file will be retrieved, if false the original will
      *                   be retrieved.
-     *
+     * @return A ParcelFileDescriptor for the wallpaper bitmap of the given destination, if it's an
+     *                   ImageWallpaper wallpaper. Return {@code null} if the wallpaper is not an
+     *                   ImageWallpaper. Also return {@code null} when called with
+     *                   which={@link #FLAG_LOCK} if there is a shared home + lock wallpaper.
      * @hide
      */
+    @FlaggedApi(FLAG_CUSTOMIZATION_PACKS_APIS)
+    @SystemApi
     @Nullable
     public ParcelFileDescriptor getWallpaperFile(@SetWallpaperFlags int which, boolean getCropped) {
         return getWallpaperFile(which, mContext.getUserId(), getCropped);
@@ -2371,7 +2435,6 @@
     /**
      * Version of setBitmap that defines how the wallpaper will be positioned for different
      * display sizes.
-     * Requires permission {@link android.Manifest.permission#SET_WALLPAPER}.
      * @param cropHints map from screen dimensions to a sub-region of the image to display for those
      *                  dimensions. The {@code Rect} sub-region may have a larger width/height ratio
      *                  than the screen dimensions to apply a horizontal parallax effect. If the
@@ -2380,6 +2443,7 @@
      * @hide
      */
     @FlaggedApi(FLAG_MULTI_CROP)
+    @TestApi
     @RequiresPermission(android.Manifest.permission.SET_WALLPAPER)
     public int setBitmapWithCrops(@Nullable Bitmap fullImage, @NonNull Map<Point, Rect> cropHints,
             boolean allowBackup, @SetWallpaperFlags int which) throws IOException {
@@ -2562,7 +2626,6 @@
     /**
      * Version of setStream that defines how the wallpaper will be positioned for different
      * display sizes.
-     * Requires permission {@link android.Manifest.permission#SET_WALLPAPER}.
      * @param cropHints map from screen dimensions to a sub-region of the image to display for those
      *                  dimensions. The {@code Rect} sub-region may have a larger width/height ratio
      *                  than the screen dimensions to apply a horizontal parallax effect. If the
@@ -2571,9 +2634,11 @@
      * @hide
      */
     @FlaggedApi(FLAG_MULTI_CROP)
+    @TestApi
     @RequiresPermission(android.Manifest.permission.SET_WALLPAPER)
-    public int setStreamWithCrops(InputStream bitmapData, @NonNull Map<Point, Rect> cropHints,
-            boolean allowBackup, @SetWallpaperFlags int which) throws IOException {
+    public int setStreamWithCrops(@NonNull InputStream bitmapData,
+            @NonNull Map<Point, Rect> cropHints, boolean allowBackup, @SetWallpaperFlags int which)
+            throws IOException {
         SparseArray<Rect> crops = new SparseArray<>();
         cropHints.forEach((k, v) -> crops.put(getOrientation(k), v));
         return setStreamWithCrops(bitmapData, crops, allowBackup, which);
@@ -2583,15 +2648,21 @@
      * Similar to {@link #setStreamWithCrops(InputStream, Map, boolean, int)}, but using
      * {@link ScreenOrientation} as keys of the cropHints map. Used for backup & restore, since
      * WallpaperBackupAgent stores orientations rather than the exact display size.
-     * Requires permission {@link android.Manifest.permission#SET_WALLPAPER}.
+     * @param bitmapData A stream containing the raw data to install as a wallpaper. This
+     *                  data can be in any format handled by {@link BitmapRegionDecoder}.
      * @param cropHints map from {@link ScreenOrientation} to a sub-region of the image to display
      *                  for that screen orientation.
+     * @param allowBackup {@code true} if the OS is permitted to back up this wallpaper
+     *     image for restore to a future device; {@code false} otherwise.
+     * @param which Flags indicating which wallpaper(s) to configure with the new imagery.
      * @hide
      */
     @FlaggedApi(FLAG_MULTI_CROP)
+    @SystemApi
     @RequiresPermission(android.Manifest.permission.SET_WALLPAPER)
-    public int setStreamWithCrops(InputStream bitmapData, @NonNull SparseArray<Rect> cropHints,
-            boolean allowBackup, @SetWallpaperFlags int which) throws IOException {
+    public int setStreamWithCrops(@NonNull InputStream bitmapData,
+            @NonNull SparseArray<Rect> cropHints, boolean allowBackup, @SetWallpaperFlags int which)
+            throws IOException {
         if (sGlobals.mService == null) {
             Log.w(TAG, "WallpaperService not running");
             throw new RuntimeException(new DeadSystemException());
diff --git a/core/java/android/app/appfunctions/AppFunctionException.java b/core/java/android/app/appfunctions/AppFunctionException.java
index d33b505..cbd1d93 100644
--- a/core/java/android/app/appfunctions/AppFunctionException.java
+++ b/core/java/android/app/appfunctions/AppFunctionException.java
@@ -141,6 +141,7 @@
      */
     public AppFunctionException(
             @ErrorCode int errorCode, @Nullable String errorMessage, @NonNull Bundle extras) {
+        super(errorMessage);
         mErrorCode = errorCode;
         mErrorMessage = errorMessage;
         mExtras = Objects.requireNonNull(extras);
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index ee93870..6934e98 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -100,16 +100,6 @@
 }
 
 flag {
-  name: "visit_person_uri"
-  namespace: "systemui"
-  description: "Guards the security fix that ensures all URIs Person.java are valid"
-  bug: "281044385"
-  metadata {
-    purpose: PURPOSE_BUGFIX
-  }
-}
-
-flag {
   name: "notification_expansion_optional"
   namespace: "systemui"
   description: "Experiment to restore the pre-S behavior where standard notifications are not expandable unless they have actions."
diff --git a/core/java/android/app/supervision/flags.aconfig b/core/java/android/app/supervision/flags.aconfig
index bcb5b36..d5e696d 100644
--- a/core/java/android/app/supervision/flags.aconfig
+++ b/core/java/android/app/supervision/flags.aconfig
@@ -7,4 +7,12 @@
   namespace: "supervision"
   description: "Flag to enable the SupervisionService"
   bug: "340351729"
-}
\ No newline at end of file
+}
+
+flag {
+  name: "supervision_api_on_wear"
+  is_exported: true
+  namespace: "supervision"
+  description: "Flag to enable the SupervisionService on Wear devices"
+  bug: "373358935"
+}
diff --git a/core/java/android/app/wallpaper.aconfig b/core/java/android/app/wallpaper.aconfig
index 4b880d0..f750a84 100644
--- a/core/java/android/app/wallpaper.aconfig
+++ b/core/java/android/app/wallpaper.aconfig
@@ -22,3 +22,21 @@
   bug: "347235611"
   is_exported: true
 }
+
+flag {
+    name: "customization_packs_apis"
+    is_exported: true
+    namespace: "systemui"
+    description: "Move APIs related to bitmap and crops to @SystemApi."
+    bug: "372344184"
+}
+
+flag {
+  name: "accurate_wallpaper_downsampling"
+  namespace: "systemui"
+  description: "Accurate downsampling of wallpaper bitmap for high resolution images"
+  bug: "355665230"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
index 34c3f57..e9e8578 100644
--- a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
+++ b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
@@ -38,6 +38,7 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.AttributeSet;
+import android.util.EmptyArray;
 import android.util.Pair;
 import android.util.Slog;
 
@@ -565,10 +566,7 @@
                                     usesSdkLibrariesVersionsMajor, usesSdkLibVersionMajor,
                                     /*allowDuplicates=*/ true);
 
-                            // We allow ":" delimiters in the SHA declaration as this is the format
-                            // emitted by the certtool making it easy for developers to copy/paste.
-                            // TODO(372862145): Add test for this replacement
-                            usesSdkCertDigest = usesSdkCertDigest.replace(":", "").toLowerCase();
+                            usesSdkCertDigest = normalizeCertDigest(usesSdkCertDigest);
 
                             if ("".equals(usesSdkCertDigest)) {
                                 // Test-only uses-sdk-library empty certificate digest override.
@@ -618,18 +616,23 @@
                                     usesStaticLibrariesVersions, usesStaticLibVersion,
                                     /*allowDuplicates=*/ true);
 
-                            // We allow ":" delimiters in the SHA declaration as this is the format
-                            // emitted by the certtool making it easy for developers to copy/paste.
-                            // TODO(372862145): Add test for this replacement
-                            usesStaticLibCertDigest =
-                                    usesStaticLibCertDigest.replace(":", "").toLowerCase();
+                            usesStaticLibCertDigest = normalizeCertDigest(usesStaticLibCertDigest);
 
-                            // TODO(372862145): Add support for multiple signer for app targeting
-                            //  O-MR1
+                            ParseResult<String[]> certResult =
+                                    parseAdditionalCertificates(input, parser);
+                            if (certResult.isError()) {
+                                return input.error(certResult);
+                            }
+                            String[] additionalCertSha256Digests = certResult.getResult();
+                            String[] certSha256Digests =
+                                    new String[additionalCertSha256Digests.length + 1];
+                            certSha256Digests[0] = usesStaticLibCertDigest;
+                            System.arraycopy(additionalCertSha256Digests, 0, certSha256Digests,
+                                    1, additionalCertSha256Digests.length);
+
                             usesStaticLibrariesCertDigests = ArrayUtils.appendElement(
                                     String[].class, usesStaticLibrariesCertDigests,
-                                    new String[]{usesStaticLibCertDigest},
-                                    /*allowDuplicates=*/ true);
+                                    certSha256Digests, /*allowDuplicates=*/ true);
                             break;
                         case TAG_SDK_LIBRARY:
                             isSdkLibrary = true;
@@ -809,6 +812,43 @@
                         declaredLibraries));
     }
 
+    private static ParseResult<String[]> parseAdditionalCertificates(ParseInput input,
+            XmlResourceParser parser) throws XmlPullParserException, IOException {
+        String[] certSha256Digests = EmptyArray.STRING;
+        final int depth = parser.getDepth();
+        int type;
+        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                && (type != XmlPullParser.END_TAG
+                || parser.getDepth() > depth)) {
+            if (type != XmlPullParser.START_TAG) {
+                continue;
+            }
+            final String nodeName = parser.getName();
+            if (nodeName.equals("additional-certificate")) {
+                String certSha256Digest = parser.getAttributeValue(
+                        ANDROID_RES_NAMESPACE, "certDigest");
+                if (TextUtils.isEmpty(certSha256Digest)) {
+                    return input.error("Bad additional-certificate declaration with empty"
+                            + " certDigest:" + certSha256Digest);
+                }
+
+                certSha256Digest = normalizeCertDigest(certSha256Digest);
+                certSha256Digests = ArrayUtils.appendElement(String.class,
+                        certSha256Digests, certSha256Digest);
+            }
+        }
+
+        return input.success(certSha256Digests);
+    }
+
+    /**
+     * We allow ":" delimiters in the SHA declaration as this is the format emitted by the
+     * certtool making it easy for developers to copy/paste.
+     */
+    private static String normalizeCertDigest(String certDigest) {
+        return certDigest.replace(":", "").toLowerCase();
+    }
+
     private static boolean isDeviceAdminReceiver(
             XmlResourceParser parser, boolean applicationHasBindDeviceAdminPermission)
             throws XmlPullParserException, IOException {
diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java
index a37648f7..4ca2005 100644
--- a/core/java/android/hardware/camera2/CameraCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraCharacteristics.java
@@ -1436,6 +1436,24 @@
             new Key<android.util.Range<Float>>("android.control.lowLightBoostInfoLuminanceRange", new TypeReference<android.util.Range<Float>>() {{ }});
 
     /**
+     * <p>List of auto-exposure priority modes for {@link CaptureRequest#CONTROL_AE_PRIORITY_MODE android.control.aePriorityMode}
+     * that are supported by this camera device.</p>
+     * <p>This entry lists the valid modes for
+     * {@link CaptureRequest#CONTROL_AE_PRIORITY_MODE android.control.aePriorityMode} for this camera device.
+     * If no AE priority modes are available for a device, this will only list OFF.</p>
+     * <p><b>Range of valid values:</b><br>
+     * Any value listed in {@link CaptureRequest#CONTROL_AE_PRIORITY_MODE android.control.aePriorityMode}</p>
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     *
+     * @see CaptureRequest#CONTROL_AE_PRIORITY_MODE
+     */
+    @PublicKey
+    @NonNull
+    @FlaggedApi(Flags.FLAG_AE_PRIORITY)
+    public static final Key<int[]> CONTROL_AE_AVAILABLE_PRIORITY_MODES =
+            new Key<int[]>("android.control.aeAvailablePriorityModes", int[].class);
+
+    /**
      * <p>List of edge enhancement modes for {@link CaptureRequest#EDGE_MODE android.edge.mode} that are supported by this camera
      * device.</p>
      * <p>Full-capability camera devices must always support OFF; camera devices that support
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index b785630..75e2058 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -994,7 +994,7 @@
         AttributionSourceState contextAttributionSourceState =
                 contextAttributionSource.asState();
 
-        if (Flags.useContextAttributionSource() && useContextAttributionSource) {
+        if (Flags.dataDeliveryPermissionChecks() && useContextAttributionSource) {
             return contextAttributionSourceState;
         } else {
             AttributionSourceState clientAttribution =
diff --git a/core/java/android/hardware/camera2/CameraMetadata.java b/core/java/android/hardware/camera2/CameraMetadata.java
index 987e2ad..8d36fbd 100644
--- a/core/java/android/hardware/camera2/CameraMetadata.java
+++ b/core/java/android/hardware/camera2/CameraMetadata.java
@@ -3371,6 +3371,74 @@
     public static final int CONTROL_AUTOFRAMING_AUTO = 2;
 
     //
+    // Enumeration values for CaptureRequest#CONTROL_ZOOM_METHOD
+    //
+
+    /**
+     * <p>The camera device automatically detects whether the application does zoom with
+     * {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} or {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}, and in turn decides which
+     * metadata tag reflects the effective zoom level.</p>
+     *
+     * @see CaptureRequest#CONTROL_ZOOM_RATIO
+     * @see CaptureRequest#SCALER_CROP_REGION
+     * @see CaptureRequest#CONTROL_ZOOM_METHOD
+     */
+    @FlaggedApi(Flags.FLAG_ZOOM_METHOD)
+    public static final int CONTROL_ZOOM_METHOD_AUTO = 0;
+
+    /**
+     * <p>The application intends to control zoom via {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}, and
+     * the effective zoom level is reflected by {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} in capture results.</p>
+     *
+     * @see CaptureRequest#CONTROL_ZOOM_RATIO
+     * @see CaptureRequest#CONTROL_ZOOM_METHOD
+     */
+    @FlaggedApi(Flags.FLAG_ZOOM_METHOD)
+    public static final int CONTROL_ZOOM_METHOD_ZOOM_RATIO = 1;
+
+    //
+    // Enumeration values for CaptureRequest#CONTROL_AE_PRIORITY_MODE
+    //
+
+    /**
+     * <p>Disable AE priority mode. This is the default value.</p>
+     * @see CaptureRequest#CONTROL_AE_PRIORITY_MODE
+     */
+    @FlaggedApi(Flags.FLAG_AE_PRIORITY)
+    public static final int CONTROL_AE_PRIORITY_MODE_OFF = 0;
+
+    /**
+     * <p>The camera device's auto-exposure routine is active and
+     * prioritizes the application-selected ISO ({@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity}).</p>
+     * <p>The application has control over {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity} while
+     * the application's values for {@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime} and
+     * {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration} are ignored.</p>
+     *
+     * @see CaptureRequest#SENSOR_EXPOSURE_TIME
+     * @see CaptureRequest#SENSOR_FRAME_DURATION
+     * @see CaptureRequest#SENSOR_SENSITIVITY
+     * @see CaptureRequest#CONTROL_AE_PRIORITY_MODE
+     */
+    @FlaggedApi(Flags.FLAG_AE_PRIORITY)
+    public static final int CONTROL_AE_PRIORITY_MODE_SENSOR_SENSITIVITY_PRIORITY = 1;
+
+    /**
+     * <p>The camera device's auto-exposure routine is active and
+     * prioritizes the application-selected exposure time
+     * ({@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime}).</p>
+     * <p>The application has control over {@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime} while
+     * the application's values for {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity} and
+     * {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration} are ignored.</p>
+     *
+     * @see CaptureRequest#SENSOR_EXPOSURE_TIME
+     * @see CaptureRequest#SENSOR_FRAME_DURATION
+     * @see CaptureRequest#SENSOR_SENSITIVITY
+     * @see CaptureRequest#CONTROL_AE_PRIORITY_MODE
+     */
+    @FlaggedApi(Flags.FLAG_AE_PRIORITY)
+    public static final int CONTROL_AE_PRIORITY_MODE_SENSOR_EXPOSURE_TIME_PRIORITY = 2;
+
+    //
     // Enumeration values for CaptureRequest#EDGE_MODE
     //
 
diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java
index 8142bbe..496d316 100644
--- a/core/java/android/hardware/camera2/CaptureRequest.java
+++ b/core/java/android/hardware/camera2/CaptureRequest.java
@@ -21,7 +21,6 @@
 import android.annotation.Nullable;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.hardware.camera2.impl.CameraMetadataNative;
-import android.hardware.camera2.impl.ExtensionKey;
 import android.hardware.camera2.impl.PublicKey;
 import android.hardware.camera2.impl.SyntheticKey;
 import android.hardware.camera2.params.OutputConfiguration;
@@ -1407,7 +1406,9 @@
      * application's selected exposure time, sensor sensitivity,
      * and frame duration ({@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime},
      * {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity}, and
-     * {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration}). If one of the FLASH modes
+     * {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration}). If {@link CaptureRequest#CONTROL_AE_PRIORITY_MODE android.control.aePriorityMode} is
+     * enabled, the relevant priority CaptureRequest settings will not be overridden.
+     * See {@link CaptureRequest#CONTROL_AE_PRIORITY_MODE android.control.aePriorityMode} for more details. If one of the FLASH modes
      * is selected, the camera device's flash unit controls are
      * also overridden.</p>
      * <p>The FLASH modes are only available if the camera device
@@ -1441,6 +1442,7 @@
      *
      * @see CameraCharacteristics#CONTROL_AE_AVAILABLE_MODES
      * @see CaptureRequest#CONTROL_AE_MODE
+     * @see CaptureRequest#CONTROL_AE_PRIORITY_MODE
      * @see CaptureRequest#CONTROL_MODE
      * @see CameraCharacteristics#FLASH_INFO_AVAILABLE
      * @see CaptureRequest#FLASH_MODE
@@ -2668,6 +2670,85 @@
             new Key<Integer>("android.control.autoframing", int.class);
 
     /**
+     * <p>Whether the application uses {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} or {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}
+     * to control zoom levels.</p>
+     * <p>If set to AUTO, the camera device detects which capture request key the application uses
+     * to do zoom, {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} or {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}. If
+     * the application doesn't set android.scaler.zoomRatio or sets it to 1.0 in the capture
+     * request, the effective zoom level is reflected in {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} in capture
+     * results. If {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} is set to values other than 1.0, the effective
+     * zoom level is reflected in {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}. AUTO is the default value
+     * for this control, and also the behavior of the OS before Android version
+     * {@link android.os.Build.VERSION_CODES#BAKLAVA BAKLAVA}.</p>
+     * <p>If set to ZOOM_RATIO, the application explicitly specifies zoom level be controlled
+     * by {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}, and the effective zoom level is reflected in
+     * {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} in capture results. This addresses an ambiguity with AUTO,
+     * with which the camera device cannot know if the application is using cropRegion or
+     * zoomRatio at 1.0x.</p>
+     * <p><b>Possible values:</b></p>
+     * <ul>
+     *   <li>{@link #CONTROL_ZOOM_METHOD_AUTO AUTO}</li>
+     *   <li>{@link #CONTROL_ZOOM_METHOD_ZOOM_RATIO ZOOM_RATIO}</li>
+     * </ul>
+     *
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     * <p><b>Limited capability</b> -
+     * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the
+     * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
+     *
+     * @see CaptureRequest#CONTROL_ZOOM_RATIO
+     * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
+     * @see CaptureRequest#SCALER_CROP_REGION
+     * @see #CONTROL_ZOOM_METHOD_AUTO
+     * @see #CONTROL_ZOOM_METHOD_ZOOM_RATIO
+     */
+    @PublicKey
+    @NonNull
+    @FlaggedApi(Flags.FLAG_ZOOM_METHOD)
+    public static final Key<Integer> CONTROL_ZOOM_METHOD =
+            new Key<Integer>("android.control.zoomMethod", int.class);
+
+    /**
+     * <p>Turn on AE priority mode.</p>
+     * <p>This control is only effective if {@link CaptureRequest#CONTROL_MODE android.control.mode} is
+     * AUTO and {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is set to one of its
+     * ON modes, with the exception of ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY.</p>
+     * <p>When a priority mode is enabled, the camera device's
+     * auto-exposure routine will maintain the application's
+     * selected parameters relevant to the priority mode while overriding
+     * the remaining exposure parameters
+     * ({@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime}, {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity}, and
+     * {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration}). For example, if
+     * SENSOR_SENSITIVITY_PRIORITY mode is enabled, the camera device will
+     * maintain the application-selected {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity}
+     * while adjusting {@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime}
+     * and {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration}. The overridden fields for a
+     * given capture will be available in its CaptureResult.</p>
+     * <p><b>Possible values:</b></p>
+     * <ul>
+     *   <li>{@link #CONTROL_AE_PRIORITY_MODE_OFF OFF}</li>
+     *   <li>{@link #CONTROL_AE_PRIORITY_MODE_SENSOR_SENSITIVITY_PRIORITY SENSOR_SENSITIVITY_PRIORITY}</li>
+     *   <li>{@link #CONTROL_AE_PRIORITY_MODE_SENSOR_EXPOSURE_TIME_PRIORITY SENSOR_EXPOSURE_TIME_PRIORITY}</li>
+     * </ul>
+     *
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     *
+     * @see CaptureRequest#CONTROL_AE_MODE
+     * @see CaptureRequest#CONTROL_MODE
+     * @see CaptureRequest#SENSOR_EXPOSURE_TIME
+     * @see CaptureRequest#SENSOR_FRAME_DURATION
+     * @see CaptureRequest#SENSOR_SENSITIVITY
+     * @see #CONTROL_AE_PRIORITY_MODE_OFF
+     * @see #CONTROL_AE_PRIORITY_MODE_SENSOR_SENSITIVITY_PRIORITY
+     * @see #CONTROL_AE_PRIORITY_MODE_SENSOR_EXPOSURE_TIME_PRIORITY
+     */
+    @PublicKey
+    @NonNull
+    @FlaggedApi(Flags.FLAG_AE_PRIORITY)
+    public static final Key<Integer> CONTROL_AE_PRIORITY_MODE =
+            new Key<Integer>("android.control.aePriorityMode", int.class);
+
+    /**
      * <p>Operation mode for edge
      * enhancement.</p>
      * <p>Edge enhancement improves sharpness and details in the captured image. OFF means
@@ -3489,7 +3570,9 @@
      * duration exposed to the nearest possible value (rather than expose longer).
      * The final exposure time used will be available in the output capture result.</p>
      * <p>This control is only effective if {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} or {@link CaptureRequest#CONTROL_MODE android.control.mode} is set to
-     * OFF; otherwise the auto-exposure algorithm will override this value.</p>
+     * OFF; otherwise the auto-exposure algorithm will override this value. However, in the
+     * case that {@link CaptureRequest#CONTROL_AE_PRIORITY_MODE android.control.aePriorityMode} is set to SENSOR_EXPOSURE_TIME_PRIORITY, this
+     * control will be effective and not controlled by the auto-exposure algorithm.</p>
      * <p><b>Units</b>: Nanoseconds</p>
      * <p><b>Range of valid values:</b><br>
      * {@link CameraCharacteristics#SENSOR_INFO_EXPOSURE_TIME_RANGE android.sensor.info.exposureTimeRange}</p>
@@ -3499,6 +3582,7 @@
      * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
      *
      * @see CaptureRequest#CONTROL_AE_MODE
+     * @see CaptureRequest#CONTROL_AE_PRIORITY_MODE
      * @see CaptureRequest#CONTROL_MODE
      * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
      * @see CameraCharacteristics#SENSOR_INFO_EXPOSURE_TIME_RANGE
@@ -3607,7 +3691,9 @@
      * value. The final sensitivity used will be available in the
      * output capture result.</p>
      * <p>This control is only effective if {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} or {@link CaptureRequest#CONTROL_MODE android.control.mode} is set to
-     * OFF; otherwise the auto-exposure algorithm will override this value.</p>
+     * OFF; otherwise the auto-exposure algorithm will override this value. However, in the
+     * case that {@link CaptureRequest#CONTROL_AE_PRIORITY_MODE android.control.aePriorityMode} is set to SENSOR_SENSITIVITY_PRIORITY, this
+     * control will be effective and not controlled by the auto-exposure algorithm.</p>
      * <p>Note that for devices supporting postRawSensitivityBoost, the total sensitivity applied
      * to the final processed image is the combination of {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity} and
      * {@link CaptureRequest#CONTROL_POST_RAW_SENSITIVITY_BOOST android.control.postRawSensitivityBoost}. In case the application uses the sensor
@@ -3623,6 +3709,7 @@
      * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
      *
      * @see CaptureRequest#CONTROL_AE_MODE
+     * @see CaptureRequest#CONTROL_AE_PRIORITY_MODE
      * @see CaptureRequest#CONTROL_MODE
      * @see CaptureRequest#CONTROL_POST_RAW_SENSITIVITY_BOOST
      * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java
index bf3a072..a52be97 100644
--- a/core/java/android/hardware/camera2/CaptureResult.java
+++ b/core/java/android/hardware/camera2/CaptureResult.java
@@ -22,7 +22,6 @@
 import android.compat.annotation.UnsupportedAppUsage;
 import android.hardware.camera2.impl.CameraMetadataNative;
 import android.hardware.camera2.impl.CaptureResultExtras;
-import android.hardware.camera2.impl.ExtensionKey;
 import android.hardware.camera2.impl.PublicKey;
 import android.hardware.camera2.impl.SyntheticKey;
 import android.hardware.camera2.utils.TypeReference;
@@ -808,7 +807,9 @@
      * application's selected exposure time, sensor sensitivity,
      * and frame duration ({@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime},
      * {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity}, and
-     * {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration}). If one of the FLASH modes
+     * {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration}). If {@link CaptureRequest#CONTROL_AE_PRIORITY_MODE android.control.aePriorityMode} is
+     * enabled, the relevant priority CaptureRequest settings will not be overridden.
+     * See {@link CaptureRequest#CONTROL_AE_PRIORITY_MODE android.control.aePriorityMode} for more details. If one of the FLASH modes
      * is selected, the camera device's flash unit controls are
      * also overridden.</p>
      * <p>The FLASH modes are only available if the camera device
@@ -842,6 +843,7 @@
      *
      * @see CameraCharacteristics#CONTROL_AE_AVAILABLE_MODES
      * @see CaptureRequest#CONTROL_AE_MODE
+     * @see CaptureRequest#CONTROL_AE_PRIORITY_MODE
      * @see CaptureRequest#CONTROL_MODE
      * @see CameraCharacteristics#FLASH_INFO_AVAILABLE
      * @see CaptureRequest#FLASH_MODE
@@ -2915,6 +2917,85 @@
             new Key<Integer>("android.control.lowLightBoostState", int.class);
 
     /**
+     * <p>Whether the application uses {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} or {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}
+     * to control zoom levels.</p>
+     * <p>If set to AUTO, the camera device detects which capture request key the application uses
+     * to do zoom, {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} or {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}. If
+     * the application doesn't set android.scaler.zoomRatio or sets it to 1.0 in the capture
+     * request, the effective zoom level is reflected in {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} in capture
+     * results. If {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} is set to values other than 1.0, the effective
+     * zoom level is reflected in {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}. AUTO is the default value
+     * for this control, and also the behavior of the OS before Android version
+     * {@link android.os.Build.VERSION_CODES#BAKLAVA BAKLAVA}.</p>
+     * <p>If set to ZOOM_RATIO, the application explicitly specifies zoom level be controlled
+     * by {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio}, and the effective zoom level is reflected in
+     * {@link CaptureRequest#CONTROL_ZOOM_RATIO android.control.zoomRatio} in capture results. This addresses an ambiguity with AUTO,
+     * with which the camera device cannot know if the application is using cropRegion or
+     * zoomRatio at 1.0x.</p>
+     * <p><b>Possible values:</b></p>
+     * <ul>
+     *   <li>{@link #CONTROL_ZOOM_METHOD_AUTO AUTO}</li>
+     *   <li>{@link #CONTROL_ZOOM_METHOD_ZOOM_RATIO ZOOM_RATIO}</li>
+     * </ul>
+     *
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     * <p><b>Limited capability</b> -
+     * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the
+     * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
+     *
+     * @see CaptureRequest#CONTROL_ZOOM_RATIO
+     * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
+     * @see CaptureRequest#SCALER_CROP_REGION
+     * @see #CONTROL_ZOOM_METHOD_AUTO
+     * @see #CONTROL_ZOOM_METHOD_ZOOM_RATIO
+     */
+    @PublicKey
+    @NonNull
+    @FlaggedApi(Flags.FLAG_ZOOM_METHOD)
+    public static final Key<Integer> CONTROL_ZOOM_METHOD =
+            new Key<Integer>("android.control.zoomMethod", int.class);
+
+    /**
+     * <p>Turn on AE priority mode.</p>
+     * <p>This control is only effective if {@link CaptureRequest#CONTROL_MODE android.control.mode} is
+     * AUTO and {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is set to one of its
+     * ON modes, with the exception of ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY.</p>
+     * <p>When a priority mode is enabled, the camera device's
+     * auto-exposure routine will maintain the application's
+     * selected parameters relevant to the priority mode while overriding
+     * the remaining exposure parameters
+     * ({@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime}, {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity}, and
+     * {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration}). For example, if
+     * SENSOR_SENSITIVITY_PRIORITY mode is enabled, the camera device will
+     * maintain the application-selected {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity}
+     * while adjusting {@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime}
+     * and {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration}. The overridden fields for a
+     * given capture will be available in its CaptureResult.</p>
+     * <p><b>Possible values:</b></p>
+     * <ul>
+     *   <li>{@link #CONTROL_AE_PRIORITY_MODE_OFF OFF}</li>
+     *   <li>{@link #CONTROL_AE_PRIORITY_MODE_SENSOR_SENSITIVITY_PRIORITY SENSOR_SENSITIVITY_PRIORITY}</li>
+     *   <li>{@link #CONTROL_AE_PRIORITY_MODE_SENSOR_EXPOSURE_TIME_PRIORITY SENSOR_EXPOSURE_TIME_PRIORITY}</li>
+     * </ul>
+     *
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     *
+     * @see CaptureRequest#CONTROL_AE_MODE
+     * @see CaptureRequest#CONTROL_MODE
+     * @see CaptureRequest#SENSOR_EXPOSURE_TIME
+     * @see CaptureRequest#SENSOR_FRAME_DURATION
+     * @see CaptureRequest#SENSOR_SENSITIVITY
+     * @see #CONTROL_AE_PRIORITY_MODE_OFF
+     * @see #CONTROL_AE_PRIORITY_MODE_SENSOR_SENSITIVITY_PRIORITY
+     * @see #CONTROL_AE_PRIORITY_MODE_SENSOR_EXPOSURE_TIME_PRIORITY
+     */
+    @PublicKey
+    @NonNull
+    @FlaggedApi(Flags.FLAG_AE_PRIORITY)
+    public static final Key<Integer> CONTROL_AE_PRIORITY_MODE =
+            new Key<Integer>("android.control.aePriorityMode", int.class);
+
+    /**
      * <p>Operation mode for edge
      * enhancement.</p>
      * <p>Edge enhancement improves sharpness and details in the captured image. OFF means
@@ -4199,7 +4280,9 @@
      * duration exposed to the nearest possible value (rather than expose longer).
      * The final exposure time used will be available in the output capture result.</p>
      * <p>This control is only effective if {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} or {@link CaptureRequest#CONTROL_MODE android.control.mode} is set to
-     * OFF; otherwise the auto-exposure algorithm will override this value.</p>
+     * OFF; otherwise the auto-exposure algorithm will override this value. However, in the
+     * case that {@link CaptureRequest#CONTROL_AE_PRIORITY_MODE android.control.aePriorityMode} is set to SENSOR_EXPOSURE_TIME_PRIORITY, this
+     * control will be effective and not controlled by the auto-exposure algorithm.</p>
      * <p><b>Units</b>: Nanoseconds</p>
      * <p><b>Range of valid values:</b><br>
      * {@link CameraCharacteristics#SENSOR_INFO_EXPOSURE_TIME_RANGE android.sensor.info.exposureTimeRange}</p>
@@ -4209,6 +4292,7 @@
      * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
      *
      * @see CaptureRequest#CONTROL_AE_MODE
+     * @see CaptureRequest#CONTROL_AE_PRIORITY_MODE
      * @see CaptureRequest#CONTROL_MODE
      * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
      * @see CameraCharacteristics#SENSOR_INFO_EXPOSURE_TIME_RANGE
@@ -4317,7 +4401,9 @@
      * value. The final sensitivity used will be available in the
      * output capture result.</p>
      * <p>This control is only effective if {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} or {@link CaptureRequest#CONTROL_MODE android.control.mode} is set to
-     * OFF; otherwise the auto-exposure algorithm will override this value.</p>
+     * OFF; otherwise the auto-exposure algorithm will override this value. However, in the
+     * case that {@link CaptureRequest#CONTROL_AE_PRIORITY_MODE android.control.aePriorityMode} is set to SENSOR_SENSITIVITY_PRIORITY, this
+     * control will be effective and not controlled by the auto-exposure algorithm.</p>
      * <p>Note that for devices supporting postRawSensitivityBoost, the total sensitivity applied
      * to the final processed image is the combination of {@link CaptureRequest#SENSOR_SENSITIVITY android.sensor.sensitivity} and
      * {@link CaptureRequest#CONTROL_POST_RAW_SENSITIVITY_BOOST android.control.postRawSensitivityBoost}. In case the application uses the sensor
@@ -4333,6 +4419,7 @@
      * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
      *
      * @see CaptureRequest#CONTROL_AE_MODE
+     * @see CaptureRequest#CONTROL_AE_PRIORITY_MODE
      * @see CaptureRequest#CONTROL_MODE
      * @see CaptureRequest#CONTROL_POST_RAW_SENSITIVITY_BOOST
      * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
diff --git a/core/java/android/hardware/devicestate/DeviceState.java b/core/java/android/hardware/devicestate/DeviceState.java
index e583627..8b4d0da 100644
--- a/core/java/android/hardware/devicestate/DeviceState.java
+++ b/core/java/android/hardware/devicestate/DeviceState.java
@@ -172,6 +172,23 @@
      */
     public static final int PROPERTY_FEATURE_DUAL_DISPLAY_INTERNAL_DEFAULT = 17;
 
+    /**
+     * Property that indicates that this state corresponds to the device state for rear display
+     * mode, where both the inner and outer displays are on. In this state, the outer display
+     * is the default display where the app is shown, and the inner display is used by the system to
+     * show a UI affordance for exiting the mode.
+     *
+     * Note that this value should generally not be used, and may be removed in the future (e.g.
+     * if or when it becomes the only type of rear display mode when
+     * {@link android.hardware.devicestate.feature.flags.Flags#deviceStateRdmV2} is removed).
+     *
+     * As such, clients should strongly consider relying on {@link #PROPERTY_FEATURE_REAR_DISPLAY}
+     * instead.
+     *
+     * @hide
+     */
+    public static final int PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT = 1001;
+
     /** @hide */
     @IntDef(prefix = {"PROPERTY_"}, flag = false, value = {
             PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED,
@@ -190,7 +207,8 @@
             PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE,
             PROPERTY_EXTENDED_DEVICE_STATE_EXTERNAL_DISPLAY,
             PROPERTY_FEATURE_REAR_DISPLAY,
-            PROPERTY_FEATURE_DUAL_DISPLAY_INTERNAL_DEFAULT
+            PROPERTY_FEATURE_DUAL_DISPLAY_INTERNAL_DEFAULT,
+            PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT
     })
     @Retention(RetentionPolicy.SOURCE)
     @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
diff --git a/core/java/android/hardware/devicestate/feature/flags.aconfig b/core/java/android/hardware/devicestate/feature/flags.aconfig
index 98ba9192..6230f4d 100644
--- a/core/java/android/hardware/devicestate/feature/flags.aconfig
+++ b/core/java/android/hardware/devicestate/feature/flags.aconfig
@@ -29,4 +29,13 @@
     metadata {
       purpose: PURPOSE_BUGFIX
     }
+}
+
+flag {
+    name: "device_state_rdm_v2"
+    is_exported: true
+    namespace: "windowing_sdk"
+    description: "Enables Rear Display Mode V2, where the inner display shows the user a UI affordance for exiting the state"
+    bug: "372486634"
+    is_fixed_read_only: true
 }
\ No newline at end of file
diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java
index 506a19c..24951c4 100644
--- a/core/java/android/hardware/input/KeyGestureEvent.java
+++ b/core/java/android/hardware/input/KeyGestureEvent.java
@@ -119,6 +119,8 @@
     public static final int KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE = 71;
     public static final int KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN = 72;
     public static final int KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT = 73;
+    public static final int KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION = 74;
+    public static final int KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK = 75;
 
     public static final int FLAG_CANCELLED = 1;
 
@@ -207,6 +209,8 @@
             KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE,
             KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN,
             KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT,
+            KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION,
+            KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface KeyGestureType {
@@ -781,6 +785,10 @@
                 return "KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN";
             case KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT:
                 return "KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_OUT";
+            case KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION:
+                return "KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION";
+            case KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK:
+                return "KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK";
             default:
                 return Integer.toHexString(value);
         }
diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig
index a8eb11d..4b2f2c2 100644
--- a/core/java/android/hardware/input/input_framework.aconfig
+++ b/core/java/android/hardware/input/input_framework.aconfig
@@ -153,8 +153,8 @@
 
 flag {
   name: "override_power_key_behavior_in_focused_window"
-  namespace: "input_native"
-  description: "Allows privileged focused windows to capture power key events."
+  namespace: "wallet_integration"
+  description: "Allows privileged focused windows to override the power key double tap behavior."
   bug: "357144512"
 }
 
diff --git a/core/java/android/hardware/usb/UsbManager.java b/core/java/android/hardware/usb/UsbManager.java
index 92608d0..d2e232a 100644
--- a/core/java/android/hardware/usb/UsbManager.java
+++ b/core/java/android/hardware/usb/UsbManager.java
@@ -54,6 +54,11 @@
 
 import com.android.internal.annotations.GuardedBy;
 
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
@@ -823,6 +828,216 @@
         }
     }
 
+    /**
+     * Opens the handle for accessory, marks it as input or output, and adds it to the map
+     * if it is the first time the accessory has had an I/O stream associated with it.
+     */
+    private AccessoryHandle openHandleForAccessory(UsbAccessory accessory,
+            boolean openingInputStream)
+            throws RemoteException {
+        synchronized (mAccessoryHandleMapLock) {
+            if (mAccessoryHandleMap == null) {
+                mAccessoryHandleMap = new ArrayMap<>();
+            }
+
+            // If accessory isn't available in map
+            if (!mAccessoryHandleMap.containsKey(accessory)) {
+                // open accessory and store associated AccessoryHandle in map
+                ParcelFileDescriptor pfd = mService.openAccessory(accessory);
+                AccessoryHandle newHandle = new AccessoryHandle(pfd, openingInputStream,
+                        !openingInputStream);
+                mAccessoryHandleMap.put(accessory, newHandle);
+
+                return newHandle;
+            }
+
+            // if accessory is already in map, get modified handle
+            AccessoryHandle currentHandle = mAccessoryHandleMap.get(accessory);
+            if (currentHandle == null) {
+                throw new IllegalStateException("Accessory doesn't have an associated handle yet!");
+            }
+
+            AccessoryHandle modifiedHandle = getModifiedHandleForOpeningStream(
+                    openingInputStream, currentHandle);
+
+            mAccessoryHandleMap.put(accessory, modifiedHandle);
+
+            return modifiedHandle;
+        }
+    }
+
+    private AccessoryHandle getModifiedHandleForOpeningStream(boolean openingInputStream,
+            @NonNull AccessoryHandle currentHandle) {
+        if (currentHandle.isInputStreamOpened() && openingInputStream) {
+            throw new IllegalStateException("Input stream already open for this accessory! "
+                    + "Please close the existing input stream before opening a new one.");
+        }
+
+        if (currentHandle.isOutputStreamOpened() && !openingInputStream) {
+            throw new IllegalStateException("Output stream already open for this accessory! "
+                    + "Please close the existing output stream before opening a new one.");
+        }
+
+        boolean isInputStreamOpened = openingInputStream || currentHandle.isInputStreamOpened();
+        boolean isOutputStreamOpened = !openingInputStream || currentHandle.isOutputStreamOpened();
+
+        return new AccessoryHandle(
+                currentHandle.getPfd(), isInputStreamOpened, isOutputStreamOpened);
+    }
+
+    /**
+     * Marks the handle for the given accessory closed for input or output, and closes the handle
+     * and removes it from the map if there are no more I/O streams associated with the handle.
+     */
+    private void closeHandleForAccessory(UsbAccessory accessory, boolean closingInputStream)
+            throws IOException {
+        synchronized (mAccessoryHandleMapLock) {
+            AccessoryHandle currentHandle = mAccessoryHandleMap.get(accessory);
+
+            if (currentHandle == null) {
+                throw new IllegalStateException(
+                        "No handle has been initialised for this accessory!");
+            }
+
+            AccessoryHandle modifiedHandle = getModifiedHandleForClosingStream(
+                    closingInputStream, currentHandle);
+            if (!modifiedHandle.isOpen()) {
+                //close handle and remove accessory handle pair from map
+                modifiedHandle.getPfd().close();
+                mAccessoryHandleMap.remove(accessory);
+            } else {
+                mAccessoryHandleMap.put(accessory, modifiedHandle);
+            }
+        }
+    }
+
+    private AccessoryHandle getModifiedHandleForClosingStream(boolean closingInputStream,
+            @NonNull AccessoryHandle currentHandle) {
+        if (!currentHandle.isInputStreamOpened() && closingInputStream) {
+            throw new IllegalStateException(
+                    "Attempting to close an input stream that has not been opened "
+                            + "for this accessory!");
+        }
+
+        if (!currentHandle.isOutputStreamOpened() && !closingInputStream) {
+            throw new IllegalStateException(
+                    "Attempting to close an output stream that has not been opened "
+                            + "for this accessory!");
+        }
+
+        boolean isInputStreamOpened = !closingInputStream && currentHandle.isInputStreamOpened();
+        boolean isOutputStreamOpened = closingInputStream && currentHandle.isOutputStreamOpened();
+
+        return new AccessoryHandle(
+                currentHandle.getPfd(), isInputStreamOpened, isOutputStreamOpened);
+    }
+
+    /**
+     * An InputStream you can create on a UsbAccessory, which will
+     * take care of calling {@link ParcelFileDescriptor#close
+     * ParcelFileDescriptor.close()} for you when the stream is closed.
+     */
+    private class AccessoryAutoCloseInputStream extends FileInputStream {
+
+        private final ParcelFileDescriptor mPfd;
+        private final UsbAccessory mAccessory;
+
+        AccessoryAutoCloseInputStream(UsbAccessory accessory, ParcelFileDescriptor pfd) {
+            super(pfd.getFileDescriptor());
+            this.mAccessory = accessory;
+            this.mPfd = pfd;
+        }
+
+        @Override
+        public void close() throws IOException {
+            /* TODO(b/377850642) : Ensure the stream is closed even if client does not
+                explicitly close the stream to avoid corrupt FDs*/
+            super.close();
+            closeHandleForAccessory(mAccessory, true);
+        }
+
+
+        @Override
+        public int read() throws IOException {
+            final int result = super.read();
+            checkError(result);
+            return result;
+        }
+
+        @Override
+        public int read(byte[] b) throws IOException {
+            final int result = super.read(b);
+            checkError(result);
+            return result;
+        }
+
+        @Override
+        public int read(byte[] b, int off, int len) throws IOException {
+            final int result = super.read(b, off, len);
+            checkError(result);
+            return result;
+        }
+
+        private void checkError(int result) throws IOException {
+            if (result == -1 && mPfd.canDetectErrors()) {
+                mPfd.checkError();
+            }
+        }
+    }
+
+    /**
+     * An OutputStream you can create on a UsbAccessory, which will
+     * take care of calling {@link ParcelFileDescriptor#close
+     * ParcelFileDescriptor.close()} for you when the stream is closed.
+     */
+    private class AccessoryAutoCloseOutputStream extends FileOutputStream {
+        private final UsbAccessory mAccessory;
+
+        AccessoryAutoCloseOutputStream(UsbAccessory accessory, ParcelFileDescriptor pfd) {
+            super(pfd.getFileDescriptor());
+            mAccessory = accessory;
+        }
+
+        @Override
+        public void close() throws IOException {
+            /* TODO(b/377850642) : Ensure the stream is closed even if client does not
+                explicitly close the stream to avoid corrupt FDs*/
+            super.close();
+            closeHandleForAccessory(mAccessory, false);
+        }
+    }
+
+    /**
+     * Holds file descriptor and marks whether input and output streams have been opened for it.
+     */
+    private static class AccessoryHandle {
+        private final ParcelFileDescriptor mPfd;
+        private final boolean mInputStreamOpened;
+        private final boolean mOutputStreamOpened;
+        AccessoryHandle(ParcelFileDescriptor parcelFileDescriptor,
+                boolean inputStreamOpened, boolean outputStreamOpened) {
+            mPfd = parcelFileDescriptor;
+            mInputStreamOpened = inputStreamOpened;
+            mOutputStreamOpened = outputStreamOpened;
+        }
+
+        public ParcelFileDescriptor getPfd() {
+            return mPfd;
+        }
+
+        public boolean isInputStreamOpened() {
+            return mInputStreamOpened;
+        }
+
+        public boolean isOutputStreamOpened() {
+            return mOutputStreamOpened;
+        }
+
+        public boolean isOpen() {
+            return (mInputStreamOpened || mOutputStreamOpened);
+        }
+    }
+
     private final Context mContext;
     private final IUsbManager mService;
     private final Object mDisplayPortListenersLock = new Object();
@@ -831,6 +1046,11 @@
     @GuardedBy("mDisplayPortListenersLock")
     private DisplayPortAltModeInfoDispatchingListener mDisplayPortServiceListener;
 
+    private final Object mAccessoryHandleMapLock = new Object();
+    @GuardedBy("mAccessoryHandleMapLock")
+    private ArrayMap<UsbAccessory, AccessoryHandle> mAccessoryHandleMap;
+
+
     /**
      * @hide
      */
@@ -922,6 +1142,10 @@
      * data of a USB transfer should be read at once. If only a partial request is read the rest of
      * the transfer is dropped.
      *
+     * <p>It is strongly recommended to use newer methods instead of this method,
+     * since this method may provide sub-optimal performance on some devices.
+     * This method could potentially face interim performance degradation as well.
+     *
      * @param accessory the USB accessory to open
      * @return file descriptor, or null if the accessory could not be opened.
      */
@@ -935,6 +1159,49 @@
     }
 
     /**
+     * Opens an input stream for reading from the USB accessory.
+     * If accessory is not open at this point, accessory will first be opened.
+     * <p>If data is read from the created {@link java.io.InputStream} all
+     * data of a USB transfer should be read at once. If only a partial request is read, the rest of
+     * the transfer is dropped.
+     * <p>The caller is responsible for ensuring that the returned stream is closed.
+     *
+     * @param accessory the USB accessory to open an input stream for
+     * @return input stream to read from given USB accessory
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_ACCESSORY_STREAM_API)
+    @RequiresFeature(PackageManager.FEATURE_USB_ACCESSORY)
+    public @NonNull InputStream openAccessoryInputStream(@NonNull UsbAccessory accessory) {
+        try {
+            return new AccessoryAutoCloseInputStream(accessory,
+                    openHandleForAccessory(accessory, true).getPfd());
+
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Opens an output stream for writing to the USB accessory.
+     * If accessory is not open at this point, accessory will first be opened.
+     * <p>The caller is responsible for ensuring that the returned stream is closed.
+     *
+     * @param accessory the USB accessory to open an output stream for
+     * @return output stream to write to given USB accessory
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_ACCESSORY_STREAM_API)
+    @RequiresFeature(PackageManager.FEATURE_USB_ACCESSORY)
+    public @NonNull OutputStream openAccessoryOutputStream(@NonNull UsbAccessory accessory) {
+        try {
+            return new AccessoryAutoCloseOutputStream(accessory,
+                    openHandleForAccessory(accessory, false).getPfd());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+
+    }
+
+    /**
      * Gets the functionfs control file descriptor for the given function, with
      * the usb descriptors and strings already written. The file descriptor is used
      * by the function implementation to handle events and control requests.
@@ -1293,7 +1560,7 @@
      * <p>
      * This function returns the current USB bandwidth through USB Gadget HAL.
      * It should be used when Android device is in USB peripheral mode and
-     * connects to a USB host. If USB state is not configued, API will return
+     * connects to a USB host. If USB state is not configured, API will return
      * {@value #USB_DATA_TRANSFER_RATE_UNKNOWN}. In addition, the unit of the
      * return value is Mbps.
      * </p>
diff --git a/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig b/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig
index 3b7a9e9..b719a7c 100644
--- a/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig
+++ b/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig
@@ -31,3 +31,11 @@
     description: "Feature flag to enable exposing usb speed system api"
     bug: "373653182"
 }
+
+flag {
+    name: "enable_accessory_stream_api"
+    is_exported: true
+    namespace: "usb"
+    description: "Feature flag to enable stream APIs for Accessory mode"
+    bug: "369356693"
+}
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index 8c3f0ef..ae83668 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -55,6 +55,7 @@
 import static android.view.inputmethod.ConnectionlessHandwritingCallback.CONNECTIONLESS_HANDWRITING_ERROR_OTHER;
 import static android.view.inputmethod.ConnectionlessHandwritingCallback.CONNECTIONLESS_HANDWRITING_ERROR_UNSUPPORTED;
 import static android.view.inputmethod.Flags.FLAG_CONNECTIONLESS_HANDWRITING;
+import static android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP_API;
 import static android.view.inputmethod.Flags.ctrlShiftShortcut;
 import static android.view.inputmethod.Flags.predictiveBackIme;
 
@@ -4392,6 +4393,39 @@
     }
 
     /**
+     * Called when the requested visibility of a custom IME Switcher button changes.
+     *
+     * <p>When the system provides an IME navigation bar, it may decide to show an IME Switcher
+     * button inside this bar. However, the IME can request hiding the bar provided by the system
+     * with {@code getWindowInsetsController().hide(captionBar())} (the IME navigation bar provides
+     * {@link Type#captionBar() captionBar} insets to the IME window). If the request is successful,
+     * then it becomes the IME's responsibility to provide a custom IME Switcher button in its
+     * input view, with equivalent functionality.</p>
+     *
+     * <p>This custom button is only requested to be visible when the system provides the IME
+     * navigation bar, both the bar and the IME Switcher button inside it should be visible,
+     * but the IME successfully requested to hide the bar. This does not depend on the current
+     * visibility of the IME. It could be called with {@code true} while the IME is hidden, in
+     * which case the IME should prepare to show the button as soon as the IME itself is shown.</p>
+     *
+     * <p>This is only called when the requested visibility changes. The default value is
+     * {@code false} and as such, this will not be called initially if the resulting value is
+     * {@code false}.</p>
+     *
+     * <p>This can be called at any time after {@link #onCreate}, even if the IME is not currently
+     * visible. However, this is not guaranteed to be called before the IME is shown, as it depends
+     * on when the IME requested hiding the IME navigation bar. If the request is sent during
+     * the showing flow (e.g. during {@link #onStartInputView}), this will be called shortly after
+     * {@link #onWindowShown}, but before the first IME frame is drawn.</p>
+     *
+     * @param visible whether the button is requested visible or not.
+     */
+    @FlaggedApi(FLAG_IME_SWITCHER_REVAMP_API)
+    public void onCustomImeSwitcherButtonRequestedVisible(boolean visible) {
+        // Intentionally empty
+    }
+
+    /**
      * Called when the IME switch button was clicked from the client. Depending on the number of
      * enabled IME subtypes, this will either switch to the next IME/subtype, or show the input
      * method picker dialog.
diff --git a/core/java/android/inputmethodservice/NavigationBarController.java b/core/java/android/inputmethodservice/NavigationBarController.java
index b08454d..38be8d9 100644
--- a/core/java/android/inputmethodservice/NavigationBarController.java
+++ b/core/java/android/inputmethodservice/NavigationBarController.java
@@ -41,6 +41,7 @@
 import android.view.WindowInsetsController.Appearance;
 import android.view.animation.Interpolator;
 import android.view.animation.PathInterpolator;
+import android.view.inputmethod.Flags;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.FrameLayout;
 
@@ -178,6 +179,9 @@
 
         private boolean mDrawLegacyNavigationBarBackground;
 
+        /** Whether a custom IME Switcher button should be visible. */
+        private boolean mCustomImeSwitcherVisible;
+
         private final Rect mTempRect = new Rect();
         private final int[] mTempPos = new int[2];
 
@@ -265,6 +269,7 @@
                     // IME navigation bar.
                     boolean visible = insets.isVisible(captionBar());
                     mNavigationBarFrame.setVisibility(visible ? View.VISIBLE : View.GONE);
+                    checkCustomImeSwitcherVisibility();
                 }
                 return view.onApplyWindowInsets(insets);
             });
@@ -491,6 +496,8 @@
                     mShouldShowImeSwitcherWhenImeIsShown;
             mShouldShowImeSwitcherWhenImeIsShown = shouldShowImeSwitcherWhenImeIsShown;
 
+            checkCustomImeSwitcherVisibility();
+
             mService.mWindow.getWindow().getDecorView().getWindowInsetsController()
                     .setImeCaptionBarInsetsHeight(getImeCaptionBarHeight(imeDrawsImeNavBar));
 
@@ -616,12 +623,33 @@
                     && mNavigationBarFrame.getVisibility() == View.VISIBLE;
         }
 
+        /**
+         * Checks if a custom IME Switcher button should be visible, and notifies the IME when this
+         * state changes. This can only be {@code true} if three conditions are met:
+         *
+         * <li>The IME should draw the IME navigation bar.</li>
+         * <li>The IME Switcher button should be visible when the IME is visible.</li>
+         * <li>The IME navigation bar should be visible, but was requested hidden by the IME.</li>
+         */
+        private void checkCustomImeSwitcherVisibility() {
+            if (!Flags.imeSwitcherRevampApi()) {
+                return;
+            }
+            final boolean visible = mImeDrawsImeNavBar && mShouldShowImeSwitcherWhenImeIsShown
+                    && mNavigationBarFrame != null && !isShown();
+            if (visible != mCustomImeSwitcherVisible) {
+                mCustomImeSwitcherVisible = visible;
+                mService.onCustomImeSwitcherButtonRequestedVisible(mCustomImeSwitcherVisible);
+            }
+        }
+
         @Override
         public String toDebugString() {
             return "{mImeDrawsImeNavBar=" + mImeDrawsImeNavBar
                     + " mNavigationBarFrame=" + mNavigationBarFrame
                     + " mShouldShowImeSwitcherWhenImeIsShown="
                     + mShouldShowImeSwitcherWhenImeIsShown
+                    + " mCustomImeSwitcherVisible="  + mCustomImeSwitcherVisible
                     + " mAppearance=0x" + Integer.toHexString(mAppearance)
                     + " mDarkIntensity=" + mDarkIntensity
                     + " mDrawLegacyNavigationBarBackground=" + mDrawLegacyNavigationBarBackground
diff --git a/core/java/android/os/WorkSource.java b/core/java/android/os/WorkSource.java
index 6d4e284..517418a 100644
--- a/core/java/android/os/WorkSource.java
+++ b/core/java/android/os/WorkSource.java
@@ -1011,13 +1011,7 @@
             return mTags.length > 0 ? mTags[0] : null;
         }
 
-        // TODO: The following three trivial getters are purely for testing and will be removed
-        // once we have higher level logic in place, e.g for serializing this WorkChain to a proto,
-        // diffing it etc.
-
-
         /** @hide */
-        @VisibleForTesting
         public int[] getUids() {
             int[] uids = new int[mSize];
             System.arraycopy(mUids, 0, uids, 0, mSize);
@@ -1025,7 +1019,6 @@
         }
 
         /** @hide */
-        @VisibleForTesting
         public String[] getTags() {
             String[] tags = new String[mSize];
             System.arraycopy(mTags, 0, tags, 0, mSize);
@@ -1033,7 +1026,6 @@
         }
 
         /** @hide */
-        @VisibleForTesting
         public int getSize() {
             return mSize;
         }
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index 9c83bc2..d9db28e 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -243,6 +243,15 @@
 }
 
 flag {
+    name: "update_engine_api"
+    namespace: "art_mainline"
+    description: "Update Engine APIs for ART"
+    is_exported: true
+    is_fixed_read_only: true
+    bug: "377557749"
+}
+
+flag {
      namespace: "system_performance"
      name: "perfetto_sdk_tracing"
      description: "Tracing using Perfetto SDK."
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index 60a0ae3..92c5c20 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -386,3 +386,17 @@
     description: "This fixed read-only flag is used to enable new ranging permission for all ranging use cases."
     bug: "370977414"
 }
+
+flag {
+    name: "system_selection_toolbar_enabled"
+    namespace: "permissions"
+    description: "Enables the system selection toolbar feature."
+    bug: "363318732"
+}
+
+flag {
+    name: "use_system_selection_toolbar_in_sysui"
+    namespace: "permissions"
+    description: "Uses the SysUi process to host the SelectionToolbarRenderService."
+    bug: "363318732"
+}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index d19681c..ef35171 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -8687,6 +8687,19 @@
         public static final String ACCESSIBILITY_QS_TARGETS = "accessibility_qs_targets";
 
         /**
+         * Setting specifying the accessibility services, accessibility shortcut targets,
+         * or features to be toggled via a keyboard shortcut gesture.
+         *
+         * <p> This is a colon-separated string list which contains the flattened
+         * {@link ComponentName} and the class name of a system class implementing a supported
+         * accessibility feature.
+         *
+         * @hide
+         */
+        public static final String ACCESSIBILITY_KEY_GESTURE_TARGETS =
+                "accessibility_key_gesture_targets";
+
+        /**
          * The system class name of magnification controller which is a target to be toggled via
          * accessibility shortcut or accessibility button.
          *
diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig
index 7cb0ffc..ce90121 100644
--- a/core/java/android/security/flags.aconfig
+++ b/core/java/android/security/flags.aconfig
@@ -109,7 +109,7 @@
 
 flag {
     name: "afl_api"
-    namespace: "platform_security"
+    namespace: "hardware_backed_security"
     description: "AFL feature"
     bug: "365994454"
 }
diff --git a/core/java/android/service/settings/preferences/ISettingsPreferenceService.aidl b/core/java/android/service/settings/preferences/ISettingsPreferenceService.aidl
new file mode 100644
index 0000000..64a8b90
--- /dev/null
+++ b/core/java/android/service/settings/preferences/ISettingsPreferenceService.aidl
@@ -0,0 +1,18 @@
+package android.service.settings.preferences;
+
+import android.service.settings.preferences.GetValueRequest;
+import android.service.settings.preferences.IGetValueCallback;
+import android.service.settings.preferences.IMetadataCallback;
+import android.service.settings.preferences.ISetValueCallback;
+import android.service.settings.preferences.MetadataRequest;
+import android.service.settings.preferences.SetValueRequest;
+
+/** @hide */
+oneway interface ISettingsPreferenceService {
+    @EnforcePermission("READ_SYSTEM_PREFERENCES")
+    void getAllPreferenceMetadata(in MetadataRequest request, IMetadataCallback callback) = 1;
+    @EnforcePermission("READ_SYSTEM_PREFERENCES")
+    void getPreferenceValue(in GetValueRequest request, IGetValueCallback callback) = 2;
+    @EnforcePermission(allOf = {"READ_SYSTEM_PREFERENCES", "WRITE_SYSTEM_PREFERENCES"})
+    void setPreferenceValue(in SetValueRequest request, ISetValueCallback callback) = 3;
+}
diff --git a/core/java/android/service/settings/preferences/SettingsPreferenceService.java b/core/java/android/service/settings/preferences/SettingsPreferenceService.java
new file mode 100644
index 0000000..4a4b5d2
--- /dev/null
+++ b/core/java/android/service/settings/preferences/SettingsPreferenceService.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.settings.preferences;
+
+import android.Manifest;
+import android.annotation.EnforcePermission;
+import android.annotation.FlaggedApi;
+import android.app.Service;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.IBinder;
+import android.os.OutcomeReceiver;
+import android.os.PermissionEnforcer;
+import android.os.RemoteException;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settingslib.flags.Flags;
+
+/**
+ * Base class for a service that exposes its settings preferences to external access.
+ * <p>This class is to be implemented by apps that contribute to the Android Settings surface.
+ * Access to this service is permission guarded by
+ * {@link android.permission.READ_SYSTEM_PREFERENCES} for binding and reading, and guarded by both
+ * {@link android.permission.READ_SYSTEM_PREFERENCES} and
+ * {@link android.permission.WRITE_SYSTEM_PREFERENCES} for writing. An additional checks for access
+ * control are the responsibility of the implementing class.
+ *
+ * <p>This implementation must correspond to an exported service declaration in the host app
+ * AndroidManifest.xml as follows
+ * <pre class="prettyprint">
+ * {@literal
+ * <service
+ *     android:permission="android.permission.READ_SYSTEM_PREFERENCES"
+ *     android:exported="true">
+ *     <intent-filter>
+ *         <action android:name="android.service.settings.preferences.action.PREFERENCE_SERVICE" />
+ *     </intent-filter>
+ * </service>}
+ * </pre>
+ *
+ * <ul>
+ *   <li>It is recommended to expose the metadata for most, if not all, preferences within a
+ *   settings app, thus implementing {@link #onGetAllPreferenceMetadata}.
+ *   <li>Exposing preferences for read access of their values is up to the implementer, but any
+ *   exposed must be a subset of the preferences exposed in {@link #onGetAllPreferenceMetadata}.
+ *   To expose a preference for read access, the implementation will contain
+ *   {@link #onGetPreferenceValue}.
+ *   <li>Exposing a preference for write access of their values is up to the implementer, but should
+ *   be done so with extra care and consideration, both for security and privacy. These must also
+ *   be a subset of those exposed in {@link #onGetAllPreferenceMetadata}. To expose a preference for
+ *   write access, the implementation will contain {@link #onSetPreferenceValue}.
+ * </ul>
+ */
+@FlaggedApi(Flags.FLAG_SETTINGS_CATALYST)
+public abstract class SettingsPreferenceService extends Service {
+
+    /**
+     * Intent Action corresponding to a {@link SettingsPreferenceService}. Note that any checks for
+     * such services must be accompanied by a check to ensure the host is a system application.
+     * Given an {@link android.content.pm.ApplicationInfo} you can check for
+     * {@link android.content.pm.ApplicationInfo#FLAG_SYSTEM}, or when querying
+     * {@link PackageManager#queryIntentServices} you can provide the flag
+     * {@link PackageManager#MATCH_SYSTEM_ONLY}.
+     */
+    public static final String ACTION_PREFERENCE_SERVICE =
+            "android.service.settings.preferences.action.PREFERENCE_SERVICE";
+
+    /** @hide */
+    @NonNull
+    @Override
+    public final IBinder onBind(@Nullable Intent intent) {
+        return new ISettingsPreferenceService.Stub(
+                PermissionEnforcer.fromContext(getApplicationContext())) {
+            @EnforcePermission(Manifest.permission.READ_SYSTEM_PREFERENCES)
+            @Override
+            public void getAllPreferenceMetadata(MetadataRequest request,
+                                                 IMetadataCallback callback) {
+                getAllPreferenceMetadata_enforcePermission();
+                onGetAllPreferenceMetadata(request, new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(MetadataResult result) {
+                        try {
+                            callback.onSuccess(result);
+                        } catch (RemoteException e) {
+                            e.rethrowFromSystemServer();
+                        }
+                    }
+
+                    @Override
+                    public void onError(@NonNull Exception error) {
+                        try {
+                            callback.onFailure();
+                        } catch (RemoteException e) {
+                            e.rethrowFromSystemServer();
+                        }
+                    }
+                });
+            }
+
+            @EnforcePermission(Manifest.permission.READ_SYSTEM_PREFERENCES)
+            @Override
+            public void getPreferenceValue(GetValueRequest request, IGetValueCallback callback) {
+                getPreferenceValue_enforcePermission();
+                onGetPreferenceValue(request, new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(GetValueResult result) {
+                        try {
+                            callback.onSuccess(result);
+                        } catch (RemoteException e) {
+                            e.rethrowFromSystemServer();
+                        }
+                    }
+
+                    @Override
+                    public void onError(@NonNull Exception error) {
+                        try {
+                            callback.onFailure();
+                        } catch (RemoteException e) {
+                            e.rethrowFromSystemServer();
+                        }
+                    }
+                });
+            }
+
+            @EnforcePermission(allOf = {
+                    Manifest.permission.READ_SYSTEM_PREFERENCES,
+                    Manifest.permission.WRITE_SYSTEM_PREFERENCES
+            })
+            @Override
+            public void setPreferenceValue(SetValueRequest request, ISetValueCallback callback) {
+                setPreferenceValue_enforcePermission();
+                onSetPreferenceValue(request, new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(SetValueResult result) {
+                        try {
+                            callback.onSuccess(result);
+                        } catch (RemoteException e) {
+                            e.rethrowFromSystemServer();
+                        }
+                    }
+
+                    @Override
+                    public void onError(@NonNull Exception error) {
+                        try {
+                            callback.onFailure();
+                        } catch (RemoteException e) {
+                            e.rethrowFromSystemServer();
+                        }
+                    }
+                });
+            }
+        };
+    }
+
+    /**
+     * Retrieve the metadata for all exposed settings preferences within this application. This
+     * data should be a snapshot of their state at the time of this method being called.
+     * @param request object to specify request parameters
+     * @param callback object to receive result or failure of request
+     */
+    public abstract void onGetAllPreferenceMetadata(
+            @NonNull MetadataRequest request,
+            @NonNull OutcomeReceiver<MetadataResult, Exception> callback);
+
+    /**
+     * Retrieve the current value of the requested settings preference. If this value is not exposed
+     * or cannot be obtained for some reason, the corresponding result code will be set on the
+     * result object.
+     * @param request object to specify request parameters
+     * @param callback object to receive result or failure of request
+     */
+    public abstract void onGetPreferenceValue(
+            @NonNull GetValueRequest request,
+            @NonNull OutcomeReceiver<GetValueResult, Exception> callback);
+
+    /**
+     * Set the value within the request to the target settings preference. If this value cannot
+     * be written for some reason, the corresponding result code will be set on the result object.
+     * @param request object to specify request parameters
+     * @param callback object to receive result or failure of request
+     */
+    public abstract void onSetPreferenceValue(
+            @NonNull SetValueRequest request,
+            @NonNull OutcomeReceiver<SetValueResult, Exception> callback);
+}
diff --git a/core/java/android/service/settings/preferences/SettingsPreferenceServiceClient.java b/core/java/android/service/settings/preferences/SettingsPreferenceServiceClient.java
new file mode 100644
index 0000000..39995a4
--- /dev/null
+++ b/core/java/android/service/settings/preferences/SettingsPreferenceServiceClient.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.settings.preferences;
+
+import static android.service.settings.preferences.SettingsPreferenceService.ACTION_PREFERENCE_SERVICE;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
+import android.annotation.TestApi;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.IBinder;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settingslib.flags.Flags;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Client class responsible for binding to and interacting with an instance of
+ * {@link SettingsPreferenceService}.
+ * <p>This is a convenience class to handle the lifecycle of the service connection.
+ * <p>This client will only interact with one instance at a time,
+ * so if the caller requires multiple instances (multiple applications that provide settings), then
+ * the caller must create multiple client classes, one for each instance required. To find all
+ * available services, a caller may query {@link android.content.pm.PackageManager} for applications
+ * that provide the intent action {@link SettingsPreferenceService#ACTION_PREFERENCE_SERVICE} that
+ * are also system applications ({@link android.content.pm.ApplicationInfo#FLAG_SYSTEM}).
+ */
+@FlaggedApi(Flags.FLAG_SETTINGS_CATALYST)
+public class SettingsPreferenceServiceClient implements AutoCloseable {
+
+    private final Context mContext;
+    private final Intent mServiceIntent;
+    private final ServiceConnection mServiceConnection;
+    private final boolean mSystemOnly;
+    private ISettingsPreferenceService mRemoteService;
+
+    /**
+     * Construct a client for binding to a {@link SettingsPreferenceService} provided by the
+     * application corresponding to the provided package name.
+     * @param packageName - package name for which this client will initiate a service binding
+     */
+    public SettingsPreferenceServiceClient(@NonNull Context context,
+                                           @NonNull String packageName) {
+        this(context, packageName, true, null);
+    }
+
+    /**
+     * @hide Only to be called directly by test
+     */
+    @TestApi
+    public SettingsPreferenceServiceClient(@NonNull Context context,
+                                           @NonNull String packageName,
+                                           boolean systemOnly,
+                                           @Nullable ServiceConnection connectionListener) {
+        mContext = context.getApplicationContext();
+        mServiceIntent = new Intent(ACTION_PREFERENCE_SERVICE).setPackage(packageName);
+        mSystemOnly = systemOnly;
+        mServiceConnection = createServiceConnection(connectionListener);
+    }
+
+    /**
+     * Initiate binding to service.
+     * <p>If no service exists for the package provided or the package is not for a system
+     * application, no binding will occur.
+     */
+    public void start() {
+        PackageManager pm = mContext.getPackageManager();
+        PackageManager.ResolveInfoFlags flags;
+        if (mSystemOnly) {
+            flags = PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_SYSTEM_ONLY);
+        } else {
+            flags = PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_ALL);
+        }
+        List<ResolveInfo> infos = pm.queryIntentServices(mServiceIntent, flags);
+        if (infos.size() == 1) {
+            mContext.bindService(mServiceIntent, mServiceConnection, Context.BIND_AUTO_CREATE);
+        }
+    }
+
+    /**
+     * If there is an active service binding, unbind from that service.
+     */
+    public void stop() {
+        if (mRemoteService != null) {
+            mRemoteService = null;
+            mContext.unbindService(mServiceConnection);
+        }
+    }
+
+    /**
+     * Retrieve the metadata for all exposed settings preferences within the application.
+     * @param request object to specify request parameters
+     * @param executor {@link Executor} on which to invoke the receiver
+     * @param receiver callback to receive the result or failure
+     */
+    public void getAllPreferenceMetadata(
+            @NonNull MetadataRequest request,
+            @CallbackExecutor @NonNull Executor executor,
+            @NonNull OutcomeReceiver<MetadataResult, Exception> receiver) {
+        if (mRemoteService == null) {
+            executor.execute(() ->
+                    receiver.onError(new IllegalStateException("Service not ready")));
+            return;
+        }
+        try {
+            mRemoteService.getAllPreferenceMetadata(request, new IMetadataCallback.Stub() {
+                @Override
+                public void onSuccess(MetadataResult result) {
+                    executor.execute(() -> receiver.onResult(result));
+                }
+
+                @Override
+                public void onFailure() {
+                    executor.execute(() -> receiver.onError(
+                            new IllegalStateException("Service call failure")));
+                }
+            });
+        } catch (RemoteException | RuntimeException e) {
+            executor.execute(() -> receiver.onError(e));
+        }
+    }
+
+    /**
+     * Retrieve the current value of the requested settings preference.
+     * @param request object to specify request parameters
+     * @param executor {@link Executor} on which to invoke the receiver
+     * @param receiver callback to receive the result or failure
+     */
+    public void getPreferenceValue(@NonNull GetValueRequest request,
+                                   @CallbackExecutor @NonNull Executor executor,
+                                   @NonNull OutcomeReceiver<GetValueResult, Exception> receiver) {
+        if (mRemoteService == null) {
+            executor.execute(() ->
+                    receiver.onError(new IllegalStateException("Service not ready")));
+            return;
+        }
+        try {
+            mRemoteService.getPreferenceValue(request, new IGetValueCallback.Stub() {
+                @Override
+                public void onSuccess(GetValueResult result) {
+                    executor.execute(() -> receiver.onResult(result));
+                }
+
+                @Override
+                public void onFailure() {
+                    executor.execute(() -> receiver.onError(
+                            new IllegalStateException("Service call failure")));
+                }
+            });
+        } catch (RemoteException | RuntimeException e) {
+            executor.execute(() -> receiver.onError(e));
+        }
+    }
+
+    /**
+     * Set the value on the target settings preference.
+     * @param request object to specify request parameters
+     * @param executor {@link Executor} on which to invoke the receiver
+     * @param receiver callback to receive the result or failure
+     */
+    public void setPreferenceValue(@NonNull SetValueRequest request,
+                                   @CallbackExecutor @NonNull Executor executor,
+                                   @NonNull OutcomeReceiver<SetValueResult, Exception> receiver) {
+        if (mRemoteService == null) {
+            executor.execute(() ->
+                    receiver.onError(new IllegalStateException("Service not ready")));
+            return;
+        }
+        try {
+            mRemoteService.setPreferenceValue(request, new ISetValueCallback.Stub() {
+                @Override
+                public void onSuccess(SetValueResult result) {
+                    executor.execute(() -> receiver.onResult(result));
+                }
+
+                @Override
+                public void onFailure() {
+                    executor.execute(() -> receiver.onError(
+                            new IllegalStateException("Service call failure")));
+                }
+            });
+        } catch (RemoteException | RuntimeException e) {
+            executor.execute(() -> receiver.onError(e));
+        }
+    }
+
+    @NonNull
+    private ServiceConnection createServiceConnection(@Nullable ServiceConnection listener) {
+        return new ServiceConnection() {
+            @Override
+            public void onServiceConnected(ComponentName name, IBinder service) {
+                mRemoteService = getPreferenceServiceInterface(service);
+                if (listener != null) {
+                    listener.onServiceConnected(name, service);
+                }
+            }
+
+            @Override
+            public void onServiceDisconnected(ComponentName name) {
+                mRemoteService = null;
+                if (listener != null) {
+                    listener.onServiceDisconnected(name);
+                }
+            }
+        };
+    }
+
+    @NonNull
+    private ISettingsPreferenceService getPreferenceServiceInterface(@NonNull IBinder service) {
+        return ISettingsPreferenceService.Stub.asInterface(service);
+    }
+
+    /**
+     * This client handles a resource, thus is it important to appropriately close that resource
+     * when it is no longer needed.
+     * <p>This method is provided by {@link AutoCloseable} and calling it
+     * will unbind any service binding.
+     */
+    @Override
+    public void close() {
+        stop();
+    }
+}
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index 02923ed..f43f172 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -163,10 +163,12 @@
 }
 
 flag {
-  name: "typeface_redesign"
+  name: "typeface_redesign_readonly"
   namespace: "text"
   description: "Decouple variation settings, weight and style information from Typeface class"
   bug: "361260253"
+  # This feature does not support runtime flag switch which leads crash in System UI.
+  is_fixed_read_only: true
 }
 
 flag {
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 9a2aa0b..75d2da1 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -9951,11 +9951,13 @@
             return false;
         }
 
-        if (!mIsDrawing) {
-            destroyHardwareRenderer();
-        } else {
-            Log.e(mTag, "Attempting to destroy the window while drawing!\n" +
-                    "  window=" + this + ", title=" + mWindowAttributes.getTitle());
+        if (!com.android.graphics.hwui.flags.Flags.removeVriSketchyDestroy()) {
+            if (!mIsDrawing) {
+                destroyHardwareRenderer();
+            } else {
+                Log.e(mTag, "Attempting to destroy the window while drawing!\n"
+                        + "  window=" + this + ", title=" + mWindowAttributes.getTitle());
+            }
         }
         mHandler.sendEmptyMessage(MSG_DIE);
         return true;
@@ -9976,9 +9978,9 @@
                 dispatchDetachedFromWindow();
             }
 
-            if (mAdded && !mFirst) {
-                destroyHardwareRenderer();
+            destroyHardwareRenderer();
 
+            if (mAdded && !mFirst) {
                 if (mView != null) {
                     int viewVisibility = mView.getVisibility();
                     boolean viewVisibilityChanged = mViewVisibility != viewVisibility;
diff --git a/core/java/android/view/autofill/AutofillFeatureFlags.java b/core/java/android/view/autofill/AutofillFeatureFlags.java
index 0ab51e4..905f350 100644
--- a/core/java/android/view/autofill/AutofillFeatureFlags.java
+++ b/core/java/android/view/autofill/AutofillFeatureFlags.java
@@ -316,6 +316,35 @@
 
     // END AUTOFILL PCC CLASSIFICATION FLAGS
 
+    // START AUTOFILL REMOVE PRE_TRIGGER FLAGS
+
+    /**
+     * Whether pre-trigger flow is disabled.
+     *
+     * @hide
+     */
+    public static final String DEVICE_CONFIG_IMPROVE_FILL_DIALOG_ENABLED = "improve_fill_dialog";
+
+    /**
+     * Minimum amount of time (in milliseconds) to wait after IME animation finishes, and before
+     * starting fill dialog animation.
+     *
+     * @hide
+     */
+    public static final String DEVICE_CONFIG_FILL_DIALOG_MIN_WAIT_AFTER_IME_ANIMATION_END_MS =
+            "fill_dialog_min_wait_after_animation_end_ms";
+
+    /**
+     * Sets a value of timeout in milliseconds, measured after animation end, during which fill
+     * dialog can be shown. If we are at time > animation_end_time + this timeout, fill dialog
+     * wouldn't be shown.
+     *
+     * @hide
+     */
+    public static final String DEVICE_CONFIG_FILL_DIALOG_TIMEOUT_MS = "fill_dialog_timeout_ms";
+
+    // END AUTOFILL REMOVE PRE_TRIGGER FLAGS
+
     /**
      * Define the max input length for autofill to show suggesiton UI
      *
@@ -366,6 +395,17 @@
             DEFAULT_AFAA_SHOULD_INCLUDE_ALL_AUTOFILL_TYPE_NOT_NONE_VIEWS_IN_ASSIST_STRUCTURE = true;
     // END AUTOFILL FOR ALL APPS DEFAULTS
 
+    // START AUTOFILL REMOVE PRE_TRIGGER FLAGS DEFAULTS
+    // Default for whether the pre trigger removal is enabled.
+    /** @hide */
+    public static final boolean DEFAULT_IMPROVE_FILL_DIALOG_ENABLED = true;
+    // Default for whether the pre trigger removal is enabled.
+    /** @hide */
+    public static final long DEFAULT_FILL_DIALOG_TIMEOUT_MS = 300; // 300 ms
+    /** @hide */
+    public static final long DEFAULT_FILL_DIALOG_MIN_WAIT_AFTER_IME_ANIMATION_END_MS = 0; // 0 ms
+    // END AUTOFILL REMOVE PRE_TRIGGER FLAGS DEFAULTS
+
     /**
      * @hide
      */
@@ -611,4 +651,48 @@
     }
 
     // END AUTOFILL PCC CLASSIFICATION FUNCTIONS
+
+
+    // START AUTOFILL REMOVE PRE_TRIGGER
+    /**
+     * Whether Autofill Pre Trigger Removal is enabled.
+     *
+     * @hide
+     */
+    public static boolean isImproveFillDialogEnabled() {
+        // TODO(b/266379948): Add condition for checking whether device has PCC first
+
+        return DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_AUTOFILL,
+                DEVICE_CONFIG_IMPROVE_FILL_DIALOG_ENABLED,
+                DEFAULT_IMPROVE_FILL_DIALOG_ENABLED);
+    }
+
+    /**
+     * Whether Autofill Pre Trigger Removal is enabled.
+     *
+     * @hide
+     */
+    public static long getFillDialogTimeoutMs() {
+        // TODO(b/266379948): Add condition for checking whether device has PCC first
+
+        return DeviceConfig.getLong(
+                DeviceConfig.NAMESPACE_AUTOFILL,
+                DEVICE_CONFIG_FILL_DIALOG_TIMEOUT_MS,
+                DEFAULT_FILL_DIALOG_TIMEOUT_MS);
+    }
+
+    /**
+     * Whether Autofill Pre Trigger Removal is enabled.
+     *
+     * @hide
+     */
+    public static long getFillDialogMinWaitAfterImeAnimationtEndMs() {
+        // TODO(b/266379948): Add condition for checking whether device has PCC first
+
+        return DeviceConfig.getLong(
+                DeviceConfig.NAMESPACE_AUTOFILL,
+                DEVICE_CONFIG_FILL_DIALOG_MIN_WAIT_AFTER_IME_ANIMATION_END_MS,
+                DEFAULT_FILL_DIALOG_MIN_WAIT_AFTER_IME_ANIMATION_END_MS);
+    }
 }
diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java
index 6b5a367..7a01ad3 100644
--- a/core/java/android/window/DesktopModeFlags.java
+++ b/core/java/android/window/DesktopModeFlags.java
@@ -54,6 +54,7 @@
             Flags::enableDesktopWindowingWallpaperActivity, true),
     ENABLE_DESKTOP_WINDOWING_MODALS_POLICY(Flags::enableDesktopWindowingModalsPolicy, true),
     ENABLE_THEMED_APP_HEADERS(Flags::enableThemedAppHeaders, true),
+    ENABLE_HOLD_TO_DRAG_APP_HANDLE(Flags::enableHoldToDragAppHandle, true),
     ENABLE_DESKTOP_WINDOWING_QUICK_SWITCH(Flags::enableDesktopWindowingQuickSwitch, true),
     ENABLE_APP_HEADER_WITH_TASK_DENSITY(Flags::enableAppHeaderWithTaskDensity, true),
     ENABLE_TASK_STACK_OBSERVER_IN_SHELL(Flags::enableTaskStackObserverInShell, true),
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index d39ecab..f474b34 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -353,6 +353,16 @@
 }
 
 flag {
+    name: "enable_desktop_system_dialogs_transitions"
+    namespace: "lse_desktop_experience"
+    description: "Enables custom transitions for system dialogs in Desktop Mode."
+    bug: "335638193"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "enable_move_to_next_display_shortcut"
     namespace: "lse_desktop_experience"
     description: "Add new keyboard shortcut of moving a task into next display"
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 68e78fe..d9de38a 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -268,6 +268,16 @@
 }
 
 flag {
+  name: "system_ui_post_animation_end"
+  namespace: "windowing_frontend"
+  description: "Run AnimatorListener#onAnimationEnd on next frame for SystemUI"
+  bug: "300035126"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
   name: "system_ui_immersive_confirmation_dialog"
   namespace: "windowing_frontend"
   description: "Enable the implementation of the immersive confirmation dialog on system UI side by default"
diff --git a/core/java/com/android/internal/accessibility/common/ShortcutConstants.java b/core/java/com/android/internal/accessibility/common/ShortcutConstants.java
index 44dceb9..4a49bb6 100644
--- a/core/java/com/android/internal/accessibility/common/ShortcutConstants.java
+++ b/core/java/com/android/internal/accessibility/common/ShortcutConstants.java
@@ -63,6 +63,8 @@
      * quickly tapping screen 2 times with two fingers as preferred shortcut.
      * {@code QUICK_SETTINGS} for displaying specifying the accessibility services or features which
      * choose Quick Settings as preferred shortcut.
+     * {@code KEY_GESTURE} for shortcuts which are directly from key gestures and should be
+     * activated always.
      */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({
@@ -73,6 +75,7 @@
             UserShortcutType.TWOFINGER_DOUBLETAP,
             UserShortcutType.QUICK_SETTINGS,
             UserShortcutType.GESTURE,
+            UserShortcutType.KEY_GESTURE,
             UserShortcutType.ALL
     })
     public @interface UserShortcutType {
@@ -84,8 +87,10 @@
         int TWOFINGER_DOUBLETAP = 1 << 3;
         int QUICK_SETTINGS = 1 << 4;
         int GESTURE = 1 << 5;
+        int KEY_GESTURE = 1 << 6;
         // LINT.ThenChange(:shortcut_type_array)
-        int ALL = SOFTWARE | HARDWARE | TRIPLETAP | TWOFINGER_DOUBLETAP | QUICK_SETTINGS | GESTURE;
+        int ALL = SOFTWARE | HARDWARE | TRIPLETAP | TWOFINGER_DOUBLETAP | QUICK_SETTINGS | GESTURE
+                | KEY_GESTURE;
     }
 
     /**
@@ -99,7 +104,8 @@
             UserShortcutType.TRIPLETAP,
             UserShortcutType.TWOFINGER_DOUBLETAP,
             UserShortcutType.QUICK_SETTINGS,
-            UserShortcutType.GESTURE
+            UserShortcutType.GESTURE,
+            UserShortcutType.KEY_GESTURE
             // LINT.ThenChange(:shortcut_type_intdef)
     };
 
diff --git a/core/java/com/android/internal/accessibility/util/ShortcutUtils.java b/core/java/com/android/internal/accessibility/util/ShortcutUtils.java
index 2e0ff3d..14ca0f8 100644
--- a/core/java/com/android/internal/accessibility/util/ShortcutUtils.java
+++ b/core/java/com/android/internal/accessibility/util/ShortcutUtils.java
@@ -27,6 +27,7 @@
 import static com.android.internal.accessibility.common.ShortcutConstants.USER_SHORTCUT_TYPES;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TRIPLETAP;
@@ -187,6 +188,7 @@
             case TWOFINGER_DOUBLETAP ->
                     Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED;
             case QUICK_SETTINGS -> Settings.Secure.ACCESSIBILITY_QS_TARGETS;
+            case KEY_GESTURE -> Settings.Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS;
             default -> throw new IllegalArgumentException(
                     "Unsupported user shortcut type: " + type);
         };
@@ -209,6 +211,7 @@
                     TRIPLETAP;
             case Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED ->
                     TWOFINGER_DOUBLETAP;
+            case Settings.Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS -> KEY_GESTURE;
             default -> throw new IllegalArgumentException(
                     "Unsupported user shortcut key: " + key);
         };
diff --git a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java
index 70b7953..c953d88 100644
--- a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java
+++ b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java
@@ -214,13 +214,30 @@
      * @param parser XML parser object currently parsing an element
      * @return true if the element is disabled because of its feature flag
      */
+    public boolean skipCurrentElement(@Nullable ParsingPackage pkg, @NonNull XmlPullParser parser) {
+        return skipCurrentElement(pkg, parser, /* allowNoNamespace= */ false);
+    }
+
+    /**
+     * Check if the element in {@code parser} should be skipped because of the feature flag.
+     * @param pkg The package being parsed
+     * @param parser XML parser object currently parsing an element
+     * @param allowNoNamespace Whether to allow namespace null
+     * @return true if the element is disabled because of its feature flag
+     */
     public boolean skipCurrentElement(
-            @NonNull ParsingPackage pkg,
-            @NonNull XmlResourceParser parser) {
+        @Nullable ParsingPackage pkg,
+        @NonNull XmlPullParser parser,
+        boolean allowNoNamespace
+    ) {
         if (!Flags.manifestFlagging()) {
             return false;
         }
         String featureFlag = parser.getAttributeValue(ANDROID_RES_NAMESPACE, "featureFlag");
+        // If allow no namespace, make another attempt to parse feature flag with null namespace.
+        if (featureFlag == null && allowNoNamespace) {
+            featureFlag = parser.getAttributeValue(null, "featureFlag");
+        }
         if (featureFlag == null) {
             return false;
         }
@@ -242,7 +259,7 @@
                     + " behind feature flag " + featureFlag + " = " + flagValue);
             shouldSkip = true;
         }
-        if (android.content.pm.Flags.includeFeatureFlagsInPackageCacher()) {
+        if (pkg != null && android.content.pm.Flags.includeFeatureFlagsInPackageCacher()) {
             pkg.addFeatureFlag(featureFlag, flagValue);
         }
         return shouldSkip;
diff --git a/core/jni/android_hardware_Camera.cpp b/core/jni/android_hardware_Camera.cpp
index 50252c1..4240614 100644
--- a/core/jni/android_hardware_Camera.cpp
+++ b/core/jni/android_hardware_Camera.cpp
@@ -538,7 +538,7 @@
         return false;
     }
 
-    if (!(useContextAttributionSource && flags::use_context_attribution_source())) {
+    if (!(useContextAttributionSource && flags::data_delivery_permission_checks())) {
         clientAttribution.uid = Camera::USE_CALLING_UID;
         clientAttribution.pid = Camera::USE_CALLING_PID;
     }
diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto
index 606e829..6af742f 100644
--- a/core/proto/android/providers/settings/secure.proto
+++ b/core/proto/android/providers/settings/secure.proto
@@ -104,6 +104,7 @@
         optional SettingProto accessibility_single_finger_panning_enabled = 56 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto accessibility_gesture_targets = 57 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto display_daltonizer_saturation_level = 58 [ (android.privacy).dest = DEST_AUTOMATIC ];
+        optional SettingProto accessibility_key_gesture_targets = 59 [ (android.privacy).dest = DEST_AUTOMATIC ];
 
     }
     optional Accessibility accessibility = 2;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index d09802a..3e0c120 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -8335,6 +8335,26 @@
         android:protectionLevel="signature|knownSigner"
         android:knownCerts="@array/config_healthConnectMigrationKnownSigners" />
 
+    <!-- @hide @SystemApi Allows permitted apps to back up Health Connect data and settings.
+             <p>Protection level: signature|knownSigner
+         @FlaggedApi("android.permission.flags.health_connect_backup_restore_permission_enabled")
+    -->
+    <permission
+        android:name="android.permission.BACKUP_HEALTH_CONNECT_DATA_AND_SETTINGS"
+        android:protectionLevel="signature|knownSigner"
+        android:knownCerts="@array/config_backupHealthConnectDataAndSettingsKnownSigners"
+        android:featureFlag="android.permission.flags.health_connect_backup_restore_permission_enabled" />
+
+    <!-- @hide @SystemApi Allows permitted apps to restore Health Connect data and settings.
+             <p>Protection level: signature|knownSigner
+         @FlaggedApi("android.permission.flags.health_connect_backup_restore_permission_enabled")
+    -->
+    <permission
+        android:name="android.permission.RESTORE_HEALTH_CONNECT_DATA_AND_SETTINGS"
+        android:protectionLevel="signature|knownSigner"
+        android:knownCerts="@array/config_restoreHealthConnectDataAndSettingsKnownSigners"
+        android:featureFlag="android.permission.flags.health_connect_backup_restore_permission_enabled" />
+
     <!-- @SystemApi Allows an app to query apps in clone profile. The permission is
          bidirectional in nature, i.e. cloned apps would be able to query apps in root user.
          The permission is not meant for 3P apps as of now.
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 7402a2f..dc054a4 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4596,6 +4596,11 @@
          exists on the device, the accessibility shortcut will be disabled by default. -->
     <string name="config_defaultAccessibilityService" translatable="false"></string>
 
+    <!-- The component name, flattened to a string, for the default select to speak service to be
+         enabled by the accessibility keyboard shortcut. If the service with the specified component
+         name is not preinstalled then this shortcut will do nothing. -->
+    <string name="config_defaultSelectToSpeakService" translatable="false"></string>
+
     <!-- URI for default Accessibility notification sound when to enable accessibility shortcut. -->
     <string name="config_defaultAccessibilityNotificationSound" translatable="false"></string>
 
@@ -7000,6 +7005,15 @@
     <string-array name="config_healthConnectMigrationKnownSigners">
     </string-array>
 
+    <!-- Certificate digests for trusted apps that will be allowed to obtain the knownSigner
+         permission for backing up HealthConnect's data and settings. The digest should be computed over the
+         DER encoding of the trusted certificate using the SHA-256 digest algorithm. -->
+    <string-array name="config_backupHealthConnectDataAndSettingsKnownSigners"/>
+    <!-- Certificate digests for trusted apps that will be allowed to obtain the knownSigner
+         permission for restoring HealthConnect's data and settings. The digest should be computed over the
+         DER encoding of the trusted certificate using the SHA-256 digest algorithm. -->
+    <string-array name="config_restoreHealthConnectDataAndSettingsKnownSigners"/>
+
     <!-- Package name of Health Connect data migrator application.  -->
     <string name="config_healthConnectMigratorPackageName"></string>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index db81a3b..badb986 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3718,6 +3718,7 @@
   <java-symbol type="string" name="color_correction_feature_name" />
   <java-symbol type="string" name="reduce_bright_colors_feature_name" />
   <java-symbol type="string" name="config_defaultAccessibilityService" />
+  <java-symbol type="string" name="config_defaultSelectToSpeakService" />
   <java-symbol type="string" name="config_defaultAccessibilityNotificationSound" />
   <java-symbol type="string" name="accessibility_shortcut_spoken_feedback" />
   <java-symbol type="array" name="config_trustedAccessibilityServices" />
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index a382d79..f39508d 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -151,6 +151,7 @@
         ":HelloWorldUsingSdk1And2",
         ":HelloWorldUsingSdkMalformedNegativeVersion",
         ":CtsStaticSharedLibConsumerApp1",
+        ":CtsStaticSharedLibConsumerApp3",
     ],
 }
 
diff --git a/core/tests/coretests/AndroidTest.xml b/core/tests/coretests/AndroidTest.xml
index 3f7c83a..5d8ff87 100644
--- a/core/tests/coretests/AndroidTest.xml
+++ b/core/tests/coretests/AndroidTest.xml
@@ -41,6 +41,8 @@
             value="/data/local/tmp/tests/coretests/pm/HelloWorldSdk1.apk"/>
         <option name="push-file" key="CtsStaticSharedLibConsumerApp1.apk"
             value="/data/local/tmp/tests/coretests/pm/CtsStaticSharedLibConsumerApp1.apk"/>
+        <option name="push-file" key="CtsStaticSharedLibConsumerApp3.apk"
+            value="/data/local/tmp/tests/coretests/pm/CtsStaticSharedLibConsumerApp3.apk"/>
     </target_preparer>
 
     <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
diff --git a/core/tests/coretests/src/android/content/pm/parsing/ApkLiteParseUtilsTest.java b/core/tests/coretests/src/android/content/pm/parsing/ApkLiteParseUtilsTest.java
index d4618d7..0db49a7 100644
--- a/core/tests/coretests/src/android/content/pm/parsing/ApkLiteParseUtilsTest.java
+++ b/core/tests/coretests/src/android/content/pm/parsing/ApkLiteParseUtilsTest.java
@@ -72,6 +72,12 @@
     private static final String TEST_APP_USING_SDK_MALFORMED_VERSION =
             "HelloWorldUsingSdkMalformedNegativeVersion.apk";
     private static final String TEST_APP_USING_STATIC_LIB = "CtsStaticSharedLibConsumerApp1.apk";
+    private static final String TEST_APP_USING_STATIC_LIB_TWO_CERTS =
+            "CtsStaticSharedLibConsumerApp3.apk";
+    private static final String STATIC_LIB_CERT_1 =
+            "70fbd440503ec0bf41f3f21fcc83ffd39880133c27deb0945ed677c6f31d72fb";
+    private static final String STATIC_LIB_CERT_2 =
+            "e49582ff3a0aa4c5589fc5feaac6b7d6e757199dd0c6742df7bf37c2ffef95f5";
     private static final String TEST_SDK1 = "HelloWorldSdk1.apk";
     private static final String TEST_SDK1_PACKAGE = "com.test.sdk1_1";
     private static final String TEST_SDK1_NAME = "com.test.sdk1";
@@ -86,7 +92,7 @@
 
     @Before
     public void setUp() throws IOException {
-        mTmpDir = mTemporaryFolder.newFolder("DexMetadataHelperTest");
+        mTmpDir = mTemporaryFolder.newFolder("ApkLiteParseUtilsTest");
     }
 
     @After
@@ -108,9 +114,8 @@
         assertThat(baseApk.getUsesSdkLibrariesVersionsMajor()).asList().containsExactly(
                 TEST_SDK1_VERSION, TEST_SDK2_VERSION
         );
-        for (String[] certDigests: baseApk.getUsesSdkLibrariesCertDigests()) {
-            assertThat(certDigests).asList().containsExactly("");
-        }
+        String[][] expectedCerts = {{""}, {""}};
+        assertThat(baseApk.getUsesSdkLibrariesCertDigests()).isEqualTo(expectedCerts);
     }
 
     @SuppressLint("CheckResult")
@@ -126,18 +131,13 @@
         ApkLite baseApk = result.getResult();
 
         String[][] liteCerts = baseApk.getUsesSdkLibrariesCertDigests();
-        assertThat(liteCerts).isNotNull();
-        for (String[] certDigests: liteCerts) {
-            assertThat(certDigests).asList().containsExactly(certDigest);
-        }
+        String[][] expectedCerts = {{certDigest}, {certDigest}};
+        assertThat(liteCerts).isEqualTo(expectedCerts);
 
         // Same for package parser
         AndroidPackage pkg = mPackageParser2.parsePackage(apkFile, 0, true).hideAsFinal();
         String[][] pkgCerts = pkg.getUsesSdkLibrariesCertDigests();
-        assertThat(pkgCerts).isNotNull();
-        for (int i = 0; i < liteCerts.length; i++) {
-            assertThat(liteCerts[i]).isEqualTo(pkgCerts[i]);
-        }
+        assertThat(liteCerts).isEqualTo(pkgCerts);
     }
 
 
@@ -160,9 +160,7 @@
 
         String[][] liteCerts = baseApk.getUsesSdkLibrariesCertDigests();
         String[][] pkgCerts = pkg.getUsesSdkLibrariesCertDigests();
-        for (int i = 0; i < liteCerts.length; i++) {
-            assertThat(liteCerts[i]).isEqualTo(pkgCerts[i]);
-        }
+        assertThat(liteCerts).isEqualTo(pkgCerts);
     }
 
     @SuppressLint("CheckResult")
@@ -184,9 +182,27 @@
 
         String[][] liteCerts = baseApk.getUsesStaticLibrariesCertDigests();
         String[][] pkgCerts = pkg.getUsesStaticLibrariesCertDigests();
-        for (int i = 0; i < liteCerts.length; i++) {
-            assertThat(liteCerts[i]).isEqualTo(pkgCerts[i]);
-        }
+        assertThat(liteCerts).isEqualTo(pkgCerts);
+    }
+
+    @Test
+    public void testParseApkLite_getUsesStaticLibrary_twoCerts()
+            throws Exception {
+        File apkFile = copyApkToTmpDir(TEST_APP_USING_STATIC_LIB_TWO_CERTS);
+        ParseResult<ApkLite> result = ApkLiteParseUtils
+                .parseApkLite(ParseTypeImpl.forDefaultParsing().reset(), apkFile, 0);
+        assertThat(result.isError()).isFalse();
+        ApkLite baseApk = result.getResult();
+
+        // There are two certs.
+        String[][] expectedCerts = {{STATIC_LIB_CERT_1, STATIC_LIB_CERT_2}};
+        String[][] liteCerts = baseApk.getUsesStaticLibrariesCertDigests();
+        assertThat(liteCerts).isEqualTo(expectedCerts);
+
+        // And they are same as package parser.
+        AndroidPackage pkg = mPackageParser2.parsePackage(apkFile, 0, true).hideAsFinal();
+        String[][] pkgCerts = pkg.getUsesStaticLibrariesCertDigests();
+        assertThat(liteCerts).isEqualTo(pkgCerts);
     }
 
     @SuppressLint("CheckResult")
diff --git a/core/tests/coretests/src/com/android/internal/accessibility/util/ShortcutUtilsTest.java b/core/tests/coretests/src/com/android/internal/accessibility/util/ShortcutUtilsTest.java
index 8bebc62..1a9af6b 100644
--- a/core/tests/coretests/src/com/android/internal/accessibility/util/ShortcutUtilsTest.java
+++ b/core/tests/coretests/src/com/android/internal/accessibility/util/ShortcutUtilsTest.java
@@ -21,6 +21,7 @@
 
 import static com.android.internal.accessibility.common.ShortcutConstants.SERVICES_SEPARATOR;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -123,6 +124,14 @@
     }
 
     @Test
+    public void getShortcutTargets_keyGestureShortcutNoService_emptyResult() {
+        assertThat(
+                ShortcutUtils.getShortcutTargetsFromSettings(
+                        mContext, KEY_GESTURE, mDefaultUserId)
+        ).isEmpty();
+    }
+
+    @Test
     public void getShortcutTargets_softwareShortcut1Service_return1Service() {
         setupShortcutTargets(ONE_COMPONENT, Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS);
         setupShortcutTargets(TWO_COMPONENTS, Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE);
diff --git a/data/etc/platform.xml b/data/etc/platform.xml
index 857df10..baaec86 100644
--- a/data/etc/platform.xml
+++ b/data/etc/platform.xml
@@ -273,6 +273,16 @@
                       targetSdk="33">
         <new-permission name="android.permission.BODY_SENSORS_BACKGROUND" />
     </split-permission>
+    <split-permission name="android.permission.BODY_SENSORS"
+                      featureFlag="android.permission.flags.replace_body_sensor_permission_enabled"
+                      targetSdk="36">
+        <new-permission name="android.permission.health.READ_HEART_RATE" />
+    </split-permission>
+    <split-permission name="android.permission.BODY_SENSORS_BACKGROUND"
+                      featureFlag="android.permission.flags.replace_body_sensor_permission_enabled"
+                      targetSdk="36">
+        <new-permission name="android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND" />
+    </split-permission>
     <split-permission name="android.permission.READ_EXTERNAL_STORAGE"
                       targetSdk="33">
         <new-permission name="android.permission.READ_MEDIA_AUDIO" />
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 7ced809..541ca60 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -594,6 +594,9 @@
         <!-- Permission required for CTS test - AdvancedProtectionManagerTest -->
         <permission name="android.permission.SET_ADVANCED_PROTECTION_MODE" />
         <permission name="android.permission.QUERY_ADVANCED_PROTECTION_MODE" />
+        <!-- Permissions required for CTS test - SettingsPreferenceServiceClientTest -->
+        <permission name="android.permission.READ_SYSTEM_PREFERENCES" />
+        <permission name="android.permission.WRITE_SYSTEM_PREFERENCES" />
     </privapp-permissions>
 
     <privapp-permissions package="com.android.statementservice">
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index 8bb32568..56bb0f0 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -2119,7 +2119,7 @@
      * @see FontVariationAxis
      */
     public boolean setFontVariationSettings(String fontVariationSettings) {
-        final boolean useFontVariationStore = Flags.typefaceRedesign()
+        final boolean useFontVariationStore = Flags.typefaceRedesignReadonly()
                 && CompatChanges.isChangeEnabled(NEW_FONT_VARIATION_MANAGEMENT);
         if (useFontVariationStore) {
             FontVariationAxis[] axes =
diff --git a/graphics/java/android/graphics/text/PositionedGlyphs.java b/graphics/java/android/graphics/text/PositionedGlyphs.java
index ed17fde..43216ba 100644
--- a/graphics/java/android/graphics/text/PositionedGlyphs.java
+++ b/graphics/java/android/graphics/text/PositionedGlyphs.java
@@ -133,7 +133,7 @@
     @NonNull
     public Font getFont(@IntRange(from = 0) int index) {
         Preconditions.checkArgumentInRange(index, 0, glyphCount() - 1, "index");
-        if (Flags.typefaceRedesign()) {
+        if (Flags.typefaceRedesignReadonly()) {
             return mFonts.get(nGetFontId(mLayoutPtr, index));
         }
         return mFonts.get(index);
@@ -252,7 +252,7 @@
         mXOffset = xOffset;
         mYOffset = yOffset;
 
-        if (Flags.typefaceRedesign()) {
+        if (Flags.typefaceRedesignReadonly()) {
             int fontCount = nGetFontCount(layoutPtr);
             mFonts = new ArrayList<>(fontCount);
             for (int i = 0; i < fontCount; ++i) {
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
index 4d7be39..76eb207 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
@@ -19,6 +19,7 @@
 import static android.hardware.devicestate.DeviceState.PROPERTY_EMULATED_ONLY;
 import static android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_DUAL_DISPLAY_INTERNAL_DEFAULT;
 import static android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_REAR_DISPLAY;
+import static android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT;
 import static android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY;
 import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
 import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER;
@@ -104,6 +105,30 @@
     @GuardedBy("mLock")
     private int mLastReportedRearDisplayPresentationStatus;
 
+    @VisibleForTesting
+    static int getRdmV1Identifier(List<DeviceState> currentSupportedDeviceStates) {
+        for (int i = 0; i < currentSupportedDeviceStates.size(); i++) {
+            DeviceState state = currentSupportedDeviceStates.get(i);
+            if (state.hasProperty(PROPERTY_FEATURE_REAR_DISPLAY)
+                    && !state.hasProperty(PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT)) {
+                return state.getIdentifier();
+            }
+        }
+        return INVALID_DEVICE_STATE_IDENTIFIER;
+    }
+
+    @VisibleForTesting
+    static int getRdmV2Identifier(List<DeviceState> currentSupportedDeviceStates) {
+        for (int i = 0; i < currentSupportedDeviceStates.size(); i++) {
+            DeviceState state = currentSupportedDeviceStates.get(i);
+            if (state.hasProperties(PROPERTY_FEATURE_REAR_DISPLAY,
+                    PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT)) {
+                return state.getIdentifier();
+            }
+        }
+        return INVALID_DEVICE_STATE_IDENTIFIER;
+    }
+
     public WindowAreaComponentImpl(@NonNull Context context) {
         mDeviceStateManager = context.getSystemService(DeviceStateManager.class);
         mDisplayManager = context.getSystemService(DisplayManager.class);
@@ -112,12 +137,10 @@
         mCurrentSupportedDeviceStates = mDeviceStateManager.getSupportedDeviceStates();
 
         if (Flags.deviceStatePropertyMigration()) {
-            for (int i = 0; i < mCurrentSupportedDeviceStates.size(); i++) {
-                DeviceState state = mCurrentSupportedDeviceStates.get(i);
-                if (state.hasProperty(PROPERTY_FEATURE_REAR_DISPLAY)) {
-                    mRearDisplayState = state.getIdentifier();
-                    break;
-                }
+            if (Flags.deviceStateRdmV2()) {
+                mRearDisplayState = getRdmV2Identifier(mCurrentSupportedDeviceStates);
+            } else {
+                mRearDisplayState = getRdmV1Identifier(mCurrentSupportedDeviceStates);
             }
         } else {
             mFoldedDeviceStates = context.getResources().getIntArray(
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/area/WindowAreaComponentImplTests.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/area/WindowAreaComponentImplTests.java
index ccb4ebe..d677fef 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/area/WindowAreaComponentImplTests.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/area/WindowAreaComponentImplTests.java
@@ -16,8 +16,13 @@
 
 package androidx.window.extensions.area;
 
+import static android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_REAR_DISPLAY;
+import static android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT;
+import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER;
+
 import static org.junit.Assert.assertEquals;
 
+import android.hardware.devicestate.DeviceState;
 import android.platform.test.annotations.Presubmit;
 import android.util.DisplayMetrics;
 import android.view.Surface;
@@ -29,11 +34,34 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
 @Presubmit
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class WindowAreaComponentImplTests {
 
+    private static final DeviceState REAR_DISPLAY_STATE_V1 = new DeviceState(
+            new DeviceState.Configuration.Builder(1, "STATE_0")
+                    .setSystemProperties(
+                            Set.of(PROPERTY_FEATURE_REAR_DISPLAY))
+                    .build());
+    private static final DeviceState REAR_DISPLAY_STATE_V2 = new DeviceState(
+            new DeviceState.Configuration.Builder(2, "STATE_0")
+                    .setSystemProperties(
+                            Set.of(PROPERTY_FEATURE_REAR_DISPLAY,
+                                    PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT))
+                    .build());
+    // The PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT state must be present together with the
+    // PROPERTY_FEATURE_REAR_DISPLAY state in order to be a valid state.
+    private static final DeviceState INVALID_REAR_DISPLAY_STATE = new DeviceState(
+            new DeviceState.Configuration.Builder(2, "STATE_0")
+                    .setSystemProperties(
+                            Set.of(PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT))
+                    .build());
+
     private final DisplayMetrics mTestDisplayMetrics = new DisplayMetrics();
 
     @Before
@@ -93,4 +121,37 @@
                 Surface.ROTATION_270, Surface.ROTATION_0, mTestDisplayMetrics);
         assertEquals(expectedMetrics, mTestDisplayMetrics);
     }
+
+    @Test
+    public void testRdmV1Identifier() {
+        final List<DeviceState> supportedStates = new ArrayList<>();
+        supportedStates.add(REAR_DISPLAY_STATE_V2);
+        assertEquals(INVALID_DEVICE_STATE_IDENTIFIER,
+                WindowAreaComponentImpl.getRdmV1Identifier(supportedStates));
+
+        supportedStates.add(REAR_DISPLAY_STATE_V1);
+        assertEquals(REAR_DISPLAY_STATE_V1.getIdentifier(),
+                WindowAreaComponentImpl.getRdmV1Identifier(supportedStates));
+    }
+
+    @Test
+    public void testRdmV2Identifier_whenStateIsImproperlyConfigured() {
+        final List<DeviceState> supportedStates = new ArrayList<>();
+        supportedStates.add(INVALID_REAR_DISPLAY_STATE);
+        assertEquals(INVALID_DEVICE_STATE_IDENTIFIER,
+                WindowAreaComponentImpl.getRdmV2Identifier(supportedStates));
+    }
+
+    @Test
+    public void testRdmV2Identifier_whenStateIsProperlyConfigured() {
+        final List<DeviceState> supportedStates = new ArrayList<>();
+
+        supportedStates.add(REAR_DISPLAY_STATE_V1);
+        assertEquals(INVALID_DEVICE_STATE_IDENTIFIER,
+                WindowAreaComponentImpl.getRdmV2Identifier(supportedStates));
+
+        supportedStates.add(REAR_DISPLAY_STATE_V2);
+        assertEquals(REAR_DISPLAY_STATE_V2.getIdentifier(),
+                WindowAreaComponentImpl.getRdmV2Identifier(supportedStates));
+    }
 }
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt
index 0b515f5..5f42bb1 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt
@@ -475,6 +475,6 @@
 
         override fun hideCurrentInputMethod() {}
 
-        override fun updateBubbleBarLocation(location: BubbleBarLocation) {}
+        override fun updateBubbleBarLocation(location: BubbleBarLocation, source: Int) {}
     }
 }
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt
index 0d742cc..6ac36a3 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt
@@ -375,7 +375,7 @@
             override fun hideCurrentInputMethod() {
             }
 
-            override fun updateBubbleBarLocation(location: BubbleBarLocation) {
+            override fun updateBubbleBarLocation(location: BubbleBarLocation, source: Int) {
             }
         }
     }
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt
index 00d9a93..0044593 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt
@@ -351,7 +351,7 @@
 
             override fun hideCurrentInputMethod() {}
 
-            override fun updateBubbleBarLocation(location: BubbleBarLocation) {}
+            override fun updateBubbleBarLocation(location: BubbleBarLocation, source: Int) {}
         }
     }
 
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
index f90e165..a18a251 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
@@ -168,7 +168,7 @@
     </LinearLayout>
 
     <LinearLayout
-        android:id="@+id/open_in_browser_pill"
+        android:id="@+id/open_in_app_or_browser_pill"
         android:layout_width="match_parent"
         android:layout_height="@dimen/desktop_mode_handle_menu_open_in_browser_pill_height"
         android:layout_marginTop="@dimen/desktop_mode_handle_menu_pill_spacing_margin"
@@ -178,7 +178,7 @@
         android:background="@drawable/desktop_mode_decor_handle_menu_background">
 
         <Button
-            android:id="@+id/open_in_browser_button"
+            android:id="@+id/open_in_app_or_browser_button"
             android:layout_weight="1"
             android:contentDescription="@string/open_in_browser_text"
             android:text="@string/open_in_browser_text"
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index 8f1ef6c..012579a 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -301,6 +301,8 @@
     <string name="screenshot_text">Screenshot</string>
     <!-- Accessibility text for the handle menu open in browser button [CHAR LIMIT=NONE] -->
     <string name="open_in_browser_text">Open in browser</string>
+    <!-- Accessibility text for the handle menu open in app button [CHAR LIMIT=NONE] -->
+    <string name="open_in_app_text">Open in App</string>
     <!-- Accessibility text for the handle menu new window button [CHAR LIMIT=NONE] -->
     <string name="new_window_text">New Window</string>
     <!-- Accessibility text for the handle menu new window button [CHAR LIMIT=NONE] -->
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt
index 191875d..84a22b8 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt
@@ -15,6 +15,7 @@
  */
 package com.android.wm.shell.shared.bubbles
 
+import android.annotation.IntDef
 import android.os.Parcel
 import android.os.Parcelable
 
@@ -60,4 +61,36 @@
             override fun newArray(size: Int) = arrayOfNulls<BubbleBarLocation>(size)
         }
     }
+
+    /** Define set of constants that allow to determine why location changed. */
+    @IntDef(
+        UpdateSource.DRAG_BAR,
+        UpdateSource.DRAG_BUBBLE,
+        UpdateSource.DRAG_EXP_VIEW,
+        UpdateSource.A11Y_ACTION_BAR,
+        UpdateSource.A11Y_ACTION_BUBBLE,
+        UpdateSource.A11Y_ACTION_EXP_VIEW,
+    )
+    @Retention(AnnotationRetention.SOURCE)
+    annotation class UpdateSource {
+        companion object {
+            /** Location changed from dragging the bar */
+            const val DRAG_BAR = 1
+
+            /** Location changed from dragging the bubble */
+            const val DRAG_BUBBLE = 2
+
+            /** Location changed from dragging the expanded view */
+            const val DRAG_EXP_VIEW = 3
+
+            /** Location changed via a11y action on the bar */
+            const val A11Y_ACTION_BAR = 4
+
+            /** Location changed via a11y action on the bubble */
+            const val A11Y_ACTION_BUBBLE = 5
+
+            /** Location changed via a11y action on the expanded view */
+            const val A11Y_ACTION_EXP_VIEW = 6
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt
index 65132fe..7243ea3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt
@@ -20,7 +20,9 @@
 
 import android.content.Context
 import android.content.Intent
+import android.content.Intent.ACTION_VIEW
 import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
+import android.content.Intent.FLAG_ACTIVITY_REQUIRE_NON_BROWSER
 import android.content.pm.PackageManager
 import android.content.pm.verify.domain.DomainVerificationManager
 import android.content.pm.verify.domain.DomainVerificationUserState
@@ -31,7 +33,7 @@
 private const val TAG = "AppToWebUtils"
 
 private val GenericBrowserIntent = Intent()
-    .setAction(Intent.ACTION_VIEW)
+    .setAction(ACTION_VIEW)
     .addCategory(Intent.CATEGORY_BROWSABLE)
     .setData(Uri.parse("http:"))
 
@@ -67,6 +69,20 @@
 }
 
 /**
+ * Returns intent if there is a non-browser application available to handle the uri. Otherwise,
+ * returns null.
+ */
+fun getAppIntent(uri: Uri, packageManager: PackageManager): Intent? {
+    val intent = Intent(ACTION_VIEW, uri).apply {
+        flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_REQUIRE_NON_BROWSER
+    }
+    // If there is no application available to handle intent, return null
+    val component = intent.resolveActivity(packageManager) ?: return null
+    intent.setComponent(component)
+    return intent
+}
+
+/**
  * Returns the [DomainVerificationUserState] of the user associated with the given
  * [DomainVerificationManager] and the given package.
  */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index c92a278..ce7a977 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -1122,7 +1122,7 @@
         final BackMotionEvent backFinish = mCurrentTracker
                 .createProgressEvent();
         dispatchOnBackProgressed(mActiveCallback, backFinish);
-        if (!mBackGestureStarted) {
+        if (mCurrentTracker.isFinished()) {
             // if the down -> up gesture happened before animation
             // start, we have to trigger the uninterruptible transition
             // to finish the back animation.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 14f8cc7..0fd98ed 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -740,8 +740,10 @@
     /**
      * Update bubble bar location and trigger and update to listeners
      */
-    public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
+    public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation,
+            @BubbleBarLocation.UpdateSource int source) {
         if (canShowAsBubbleBar()) {
+            BubbleBarLocation previousLocation = mBubblePositioner.getBubbleBarLocation();
             mBubblePositioner.setBubbleBarLocation(bubbleBarLocation);
             if (mLayerView != null && !mLayerView.isExpandedViewDragged()) {
                 mLayerView.updateExpandedView();
@@ -749,13 +751,47 @@
             BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate();
             bubbleBarUpdate.bubbleBarLocation = bubbleBarLocation;
             mBubbleStateListener.onBubbleStateChange(bubbleBarUpdate);
+
+            logBubbleBarLocationIfChanged(bubbleBarLocation, previousLocation, source);
+        }
+    }
+
+    private void logBubbleBarLocationIfChanged(BubbleBarLocation location,
+            BubbleBarLocation previous,
+            @BubbleBarLocation.UpdateSource int source) {
+        if (mLayerView == null) {
+            return;
+        }
+        boolean isRtl = mLayerView.isLayoutRtl();
+        boolean wasLeft = previous.isOnLeft(isRtl);
+        boolean onLeft = location.isOnLeft(isRtl);
+        if (wasLeft == onLeft) {
+            // No changes, skip logging
+            return;
+        }
+        switch (source) {
+            case BubbleBarLocation.UpdateSource.DRAG_BAR:
+            case BubbleBarLocation.UpdateSource.A11Y_ACTION_BAR:
+                mLogger.log(onLeft ? BubbleLogger.Event.BUBBLE_BAR_MOVED_LEFT_DRAG_BAR
+                        : BubbleLogger.Event.BUBBLE_BAR_MOVED_RIGHT_DRAG_BAR);
+                break;
+            case BubbleBarLocation.UpdateSource.DRAG_BUBBLE:
+            case BubbleBarLocation.UpdateSource.A11Y_ACTION_BUBBLE:
+                mLogger.log(onLeft ? BubbleLogger.Event.BUBBLE_BAR_MOVED_LEFT_DRAG_BUBBLE
+                        : BubbleLogger.Event.BUBBLE_BAR_MOVED_RIGHT_DRAG_BUBBLE);
+                break;
+            case BubbleBarLocation.UpdateSource.DRAG_EXP_VIEW:
+            case BubbleBarLocation.UpdateSource.A11Y_ACTION_EXP_VIEW:
+                // TODO(b/349845968): move logging from BubbleBarLayerView to here
+                break;
         }
     }
 
     /**
      * Animate bubble bar to the given location. The location change is transient. It does not
      * update the state of the bubble bar.
-     * To update bubble bar pinned location, use {@link #setBubbleBarLocation(BubbleBarLocation)}.
+     * To update bubble bar pinned location, use
+     * {@link #setBubbleBarLocation(BubbleBarLocation, int)}.
      */
     public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
         if (canShowAsBubbleBar()) {
@@ -2568,9 +2604,10 @@
         }
 
         @Override
-        public void setBubbleBarLocation(BubbleBarLocation location) {
+        public void setBubbleBarLocation(BubbleBarLocation location,
+                @BubbleBarLocation.UpdateSource int source) {
             mMainExecutor.execute(() ->
-                    mController.setBubbleBarLocation(location));
+                    mController.setBubbleBarLocation(location, source));
         }
 
         @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt
index ec4854b..6423eed 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt
@@ -32,7 +32,10 @@
     fun isStackExpanded(): Boolean
     fun isShowingAsBubbleBar(): Boolean
     fun hideCurrentInputMethod()
-    fun updateBubbleBarLocation(location: BubbleBarLocation)
+    fun updateBubbleBarLocation(
+        location: BubbleBarLocation,
+        @BubbleBarLocation.UpdateSource source: Int,
+    )
 
     companion object {
         /**
@@ -82,8 +85,11 @@
                     controller.hideCurrentInputMethod()
                 }
 
-                override fun updateBubbleBarLocation(location: BubbleBarLocation) {
-                    controller.bubbleBarLocation = location
+                override fun updateBubbleBarLocation(
+                    location: BubbleBarLocation,
+                    @BubbleBarLocation.UpdateSource source: Int,
+                ) {
+                    controller.setBubbleBarLocation(location, source)
                 }
             }
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
index 1855b93..9c2d3543 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
@@ -44,7 +44,7 @@
 
     oneway void showUserEducation(in int positionX, in int positionY) = 8;
 
-    oneway void setBubbleBarLocation(in BubbleBarLocation location) = 9;
+    oneway void setBubbleBarLocation(in BubbleBarLocation location, in int source) = 9;
 
     oneway void updateBubbleBarTopOnScreen(in int topOnScreen) = 10;
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
index 272dfec..3764bcd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
@@ -637,11 +637,13 @@
                 return true;
             }
             if (action == R.id.action_move_bubble_bar_left) {
-                mManager.updateBubbleBarLocation(BubbleBarLocation.LEFT);
+                mManager.updateBubbleBarLocation(BubbleBarLocation.LEFT,
+                        BubbleBarLocation.UpdateSource.A11Y_ACTION_EXP_VIEW);
                 return true;
             }
             if (action == R.id.action_move_bubble_bar_right) {
-                mManager.updateBubbleBarLocation(BubbleBarLocation.RIGHT);
+                mManager.updateBubbleBarLocation(BubbleBarLocation.RIGHT,
+                        BubbleBarLocation.UpdateSource.A11Y_ACTION_EXP_VIEW);
                 return true;
             }
             return false;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index 1f77abe..0c05e3c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -441,7 +441,8 @@
 
         @Override
         public void onRelease(@NonNull BubbleBarLocation location) {
-            mBubbleController.setBubbleBarLocation(location);
+            mBubbleController.setBubbleBarLocation(location,
+                    BubbleBarLocation.UpdateSource.DRAG_EXP_VIEW);
             if (location != mInitialLocation) {
                 BubbleLogger.Event event = location.isOnLeft(isLayoutRtl())
                         ? BubbleLogger.Event.BUBBLE_BAR_MOVED_LEFT_DRAG_EXP_VIEW
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 44fce81..601cf70 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
@@ -67,6 +67,7 @@
 import com.android.wm.shell.desktopmode.CloseDesktopTaskTransitionHandler;
 import com.android.wm.shell.desktopmode.DefaultDragToDesktopTransitionHandler;
 import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler;
+import com.android.wm.shell.desktopmode.DesktopBackNavigationTransitionHandler;
 import com.android.wm.shell.desktopmode.DesktopDisplayEventHandler;
 import com.android.wm.shell.desktopmode.DesktopImmersiveController;
 import com.android.wm.shell.desktopmode.DesktopMixedTransitionHandler;
@@ -915,6 +916,16 @@
 
     @WMSingleton
     @Provides
+    static DesktopBackNavigationTransitionHandler provideDesktopBackNavigationTransitionHandler(
+            @ShellMainThread ShellExecutor mainExecutor,
+            @ShellAnimationThread ShellExecutor animExecutor,
+            DisplayController displayController) {
+        return new DesktopBackNavigationTransitionHandler(mainExecutor, animExecutor,
+                displayController);
+    }
+
+    @WMSingleton
+    @Provides
     static DesktopModeDragAndDropTransitionHandler provideDesktopModeDragAndDropTransitionHandler(
             Transitions transitions) {
         return new DesktopModeDragAndDropTransitionHandler(transitions);
@@ -964,6 +975,7 @@
             Optional<DesktopRepository> desktopRepository,
             Transitions transitions,
             ShellTaskOrganizer shellTaskOrganizer,
+            Optional<DesktopMixedTransitionHandler> desktopMixedTransitionHandler,
             ShellInit shellInit) {
         return desktopRepository.flatMap(
                 repository ->
@@ -973,6 +985,7 @@
                                         repository,
                                         transitions,
                                         shellTaskOrganizer,
+                                        desktopMixedTransitionHandler.get(),
                                         shellInit)));
     }
 
@@ -985,6 +998,7 @@
             FreeformTaskTransitionHandler freeformTaskTransitionHandler,
             CloseDesktopTaskTransitionHandler closeDesktopTaskTransitionHandler,
             Optional<DesktopImmersiveController> desktopImmersiveController,
+            DesktopBackNavigationTransitionHandler desktopBackNavigationTransitionHandler,
             InteractionJankMonitor interactionJankMonitor,
             @ShellMainThread Handler handler,
             ShellInit shellInit,
@@ -1001,6 +1015,7 @@
                         freeformTaskTransitionHandler,
                         closeDesktopTaskTransitionHandler,
                         desktopImmersiveController.get(),
+                        desktopBackNavigationTransitionHandler,
                         interactionJankMonitor,
                         handler,
                         shellInit,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandler.kt
new file mode 100644
index 0000000..83b0f84
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandler.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.desktopmode
+
+import android.animation.Animator
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.os.IBinder
+import android.util.DisplayMetrics
+import android.view.SurfaceControl.Transaction
+import android.window.TransitionInfo
+import android.window.TransitionRequestInfo
+import android.window.WindowContainerTransaction
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.shared.TransitionUtil
+import com.android.wm.shell.shared.animation.MinimizeAnimator.create
+import com.android.wm.shell.transition.Transitions
+
+/**
+ * The [Transitions.TransitionHandler] that handles transitions for tasks that are closing or going
+ * to back as part of back navigation. This handler is used only for animating transitions.
+ */
+class DesktopBackNavigationTransitionHandler(
+    private val mainExecutor: ShellExecutor,
+    private val animExecutor: ShellExecutor,
+    private val displayController: DisplayController,
+) : Transitions.TransitionHandler {
+
+    /** Shouldn't handle anything */
+    override fun handleRequest(
+        transition: IBinder,
+        request: TransitionRequestInfo,
+    ): WindowContainerTransaction? = null
+
+    /** Animates a transition with minimizing tasks */
+    override fun startAnimation(
+        transition: IBinder,
+        info: TransitionInfo,
+        startTransaction: Transaction,
+        finishTransaction: Transaction,
+        finishCallback: Transitions.TransitionFinishCallback,
+    ): Boolean {
+        if (!TransitionUtil.isClosingType(info.type)) return false
+
+        val animations = mutableListOf<Animator>()
+        val onAnimFinish: (Animator) -> Unit = { animator ->
+            mainExecutor.execute {
+                // Animation completed
+                animations.remove(animator)
+                if (animations.isEmpty()) {
+                    // All animations completed, finish the transition
+                    finishCallback.onTransitionFinished(/* wct= */ null)
+                }
+            }
+        }
+
+        animations +=
+            info.changes
+                .filter {
+                    it.mode == info.type &&
+                            it.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM
+                }
+                .mapNotNull { createMinimizeAnimation(it, finishTransaction, onAnimFinish) }
+        if (animations.isEmpty()) return false
+        animExecutor.execute { animations.forEach(Animator::start) }
+        return true
+    }
+
+    private fun createMinimizeAnimation(
+        change: TransitionInfo.Change,
+        finishTransaction: Transaction,
+        onAnimFinish: (Animator) -> Unit
+    ): Animator? {
+        val t = Transaction()
+        val sc = change.leash
+        finishTransaction.hide(sc)
+        val displayMetrics: DisplayMetrics? =
+            change.taskInfo?.let {
+                displayController.getDisplayContext(it.displayId)?.getResources()?.displayMetrics
+            }
+        return displayMetrics?.let { create(it, change, t, onAnimFinish) }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
index 01c680d..2001f97 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
@@ -52,6 +52,7 @@
     private val freeformTaskTransitionHandler: FreeformTaskTransitionHandler,
     private val closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler,
     private val desktopImmersiveController: DesktopImmersiveController,
+    private val desktopBackNavigationTransitionHandler: DesktopBackNavigationTransitionHandler,
     private val interactionJankMonitor: InteractionJankMonitor,
     @ShellMainThread private val handler: Handler,
     shellInit: ShellInit,
@@ -161,6 +162,14 @@
                 finishTransaction,
                 finishCallback
             )
+            is PendingMixedTransition.Minimize -> animateMinimizeTransition(
+                pending,
+                transition,
+                info,
+                startTransaction,
+                finishTransaction,
+                finishCallback
+            )
         }
     }
 
@@ -272,6 +281,42 @@
         )
     }
 
+    private fun animateMinimizeTransition(
+        pending: PendingMixedTransition.Minimize,
+        transition: IBinder,
+        info: TransitionInfo,
+        startTransaction: SurfaceControl.Transaction,
+        finishTransaction: SurfaceControl.Transaction,
+        finishCallback: TransitionFinishCallback,
+    ): Boolean {
+        if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue) return false
+
+        val minimizeChange = findDesktopTaskChange(info, pending.minimizingTask)
+        if (minimizeChange == null) {
+            logW("Should have minimizing desktop task")
+            return false
+        }
+        if (pending.isLastTask) {
+            // Dispatch close desktop task animation to the default transition handlers.
+            return dispatchToLeftoverHandler(
+                transition,
+                info,
+                startTransaction,
+                finishTransaction,
+                finishCallback
+            )
+        }
+
+        // Animate minimizing desktop task transition with [DesktopBackNavigationTransitionHandler].
+        return desktopBackNavigationTransitionHandler.startAnimation(
+            transition,
+            info,
+            startTransaction,
+            finishTransaction,
+            finishCallback,
+        )
+    }
+
     override fun onTransitionConsumed(
         transition: IBinder,
         aborted: Boolean,
@@ -400,6 +445,14 @@
             val minimizingTask: Int?,
             val exitingImmersiveTask: Int?,
         ) : PendingMixedTransition()
+
+        /** A task is minimizing. This should be used for task going to back and some closing cases
+         * with back navigation. */
+        data class Minimize(
+            override val transition: IBinder,
+            val minimizingTask: Int,
+            val isLastTask: Boolean,
+        ) : PendingMixedTransition()
     }
 
     private fun logV(msg: String, vararg arguments: Any?) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
index bed484c..39586e3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
@@ -594,6 +594,10 @@
                 FrameworkStatsLog
                     .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__MAXIMIZE_MENU_RESIZE_TRIGGER
             ),
+            DRAG_TO_TOP_RESIZE_TRIGGER(
+                FrameworkStatsLog
+                    .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__DRAG_TO_TOP_RESIZE_TRIGGER
+            ),
         }
 
         /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 927fd88..223038f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -871,11 +871,10 @@
             return
         }
 
-        // TODO(b/375356605): Introduce a new ResizeTrigger for drag-to-top.
         desktopModeEventLogger.logTaskResizingStarted(
-            ResizeTrigger.UNKNOWN_RESIZE_TRIGGER, motionEvent, taskInfo, displayController
+            ResizeTrigger.DRAG_TO_TOP_RESIZE_TRIGGER, motionEvent, taskInfo, displayController
         )
-        toggleDesktopTaskSize(taskInfo, ResizeTrigger.UNKNOWN_RESIZE_TRIGGER, motionEvent)
+        toggleDesktopTaskSize(taskInfo, ResizeTrigger.DRAG_TO_TOP_RESIZE_TRIGGER, motionEvent)
     }
 
     private fun getMaximizeBounds(taskInfo: RunningTaskInfo, stableBounds: Rect): Rect {
@@ -1291,7 +1290,11 @@
                     // Check if freeform task launch during recents should be handled
                     shouldHandleMidRecentsFreeformLaunch -> handleMidRecentsFreeformTaskLaunch(task)
                     // Check if the closing task needs to be handled
-                    TransitionUtil.isClosingType(request.type) -> handleTaskClosing(task)
+                    TransitionUtil.isClosingType(request.type) -> handleTaskClosing(
+                        task,
+                        transition,
+                        request.type
+                    )
                     // Check if the top task shouldn't be allowed to enter desktop mode
                     isIncompatibleTask(task) -> handleIncompatibleTaskLaunch(task)
                     // Check if fullscreen task should be updated
@@ -1621,7 +1624,7 @@
     }
 
     /** Handle task closing by removing wallpaper activity if it's the last active task */
-    private fun handleTaskClosing(task: RunningTaskInfo): WindowContainerTransaction? {
+    private fun handleTaskClosing(task: RunningTaskInfo, transition: IBinder, requestType: Int): WindowContainerTransaction? {
         logV("handleTaskClosing")
         if (!isDesktopModeShowing(task.displayId))
             return null
@@ -1637,8 +1640,15 @@
         if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) {
             taskRepository.addClosingTask(task.displayId, task.taskId)
             desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, task.taskId)
+        } else if (requestType == TRANSIT_CLOSE) {
+            // Handle closing tasks, tasks that are going to back are handled in
+            // [DesktopTasksTransitionObserver].
+            desktopMixedTransitionHandler.addPendingMixedTransition(
+                DesktopMixedTransitionHandler.PendingMixedTransition.Minimize(
+                    transition, task.taskId, taskRepository.getVisibleTaskCount(task.displayId) == 1
+                )
+            )
         }
-
         taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(
             doesAnyTaskRequireTaskbarRounding(
                 task.displayId,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
index d1534da..c39c715 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
@@ -46,6 +46,7 @@
     private val desktopRepository: DesktopRepository,
     private val transitions: Transitions,
     private val shellTaskOrganizer: ShellTaskOrganizer,
+    private val desktopMixedTransitionHandler: DesktopMixedTransitionHandler,
     shellInit: ShellInit
 ) : Transitions.TransitionObserver {
 
@@ -71,7 +72,7 @@
         // TODO: b/332682201 Update repository state
         updateWallpaperToken(info)
         if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) {
-            handleBackNavigation(info)
+            handleBackNavigation(transition, info)
             removeTaskIfNeeded(info)
         }
         removeWallpaperOnLastTaskClosingIfNeeded(transition, info)
@@ -95,7 +96,7 @@
         }
     }
 
-    private fun handleBackNavigation(info: TransitionInfo) {
+    private fun handleBackNavigation(transition: IBinder, info: TransitionInfo) {
         // When default back navigation happens, transition type is TO_BACK and the change is
         // TO_BACK. Mark the task going to back as minimized.
         if (info.type == TRANSIT_TO_BACK) {
@@ -105,10 +106,14 @@
                     continue
                 }
 
-                if (desktopRepository.getVisibleTaskCount(taskInfo.displayId) > 0 &&
+                val visibleTaskCount = desktopRepository.getVisibleTaskCount(taskInfo.displayId)
+                if (visibleTaskCount > 0 &&
                     change.mode == TRANSIT_TO_BACK &&
                     taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) {
                     desktopRepository.minimizeTask(taskInfo.displayId, taskInfo.taskId)
+                    desktopMixedTransitionHandler.addPendingMixedTransition(
+                        DesktopMixedTransitionHandler.PendingMixedTransition.Minimize(
+                            transition, taskInfo.taskId, visibleTaskCount == 1))
                 }
             }
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
index 4aeecbe..5276d9d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
@@ -27,6 +27,7 @@
 import android.animation.ValueAnimator;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.app.AppCompatTaskInfo;
 import android.app.TaskInfo;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
@@ -176,12 +177,12 @@
     public PipTransitionAnimator getAnimator(TaskInfo taskInfo, SurfaceControl leash,
             Rect baseBounds, Rect startBounds, Rect endBounds, Rect sourceHintRect,
             @PipAnimationController.TransitionDirection int direction, float startingAngle,
-            @Surface.Rotation int rotationDelta) {
+            @Surface.Rotation int rotationDelta, boolean alwaysAnimateTaskBounds) {
         if (mCurrentAnimator == null) {
             mCurrentAnimator = setupPipTransitionAnimator(
                     PipTransitionAnimator.ofBounds(taskInfo, leash, startBounds, startBounds,
                             endBounds, sourceHintRect, direction, 0 /* startingAngle */,
-                            rotationDelta));
+                            rotationDelta, alwaysAnimateTaskBounds));
         } else if (mCurrentAnimator.getAnimationType() == ANIM_TYPE_ALPHA
                 && mCurrentAnimator.isRunning()) {
             // If we are still animating the fade into pip, then just move the surface and ensure
@@ -197,7 +198,8 @@
             mCurrentAnimator.cancel();
             mCurrentAnimator = setupPipTransitionAnimator(
                     PipTransitionAnimator.ofBounds(taskInfo, leash, baseBounds, startBounds,
-                            endBounds, sourceHintRect, direction, startingAngle, rotationDelta));
+                            endBounds, sourceHintRect, direction, startingAngle, rotationDelta,
+                            alwaysAnimateTaskBounds));
         }
         return mCurrentAnimator;
     }
@@ -585,28 +587,32 @@
         static PipTransitionAnimator<Rect> ofBounds(TaskInfo taskInfo, SurfaceControl leash,
                 Rect baseValue, Rect startValue, Rect endValue, Rect sourceRectHint,
                 @PipAnimationController.TransitionDirection int direction, float startingAngle,
-                @Surface.Rotation int rotationDelta) {
+                @Surface.Rotation int rotationDelta, boolean alwaysAnimateTaskBounds) {
             final boolean isOutPipDirection = isOutPipDirection(direction);
             final boolean isInPipDirection = isInPipDirection(direction);
             // Just for simplicity we'll interpolate between the source rect hint insets and empty
             // insets to calculate the window crop
             final Rect initialSourceValue;
             final Rect mainWindowFrame = taskInfo.topActivityMainWindowFrame;
-            final boolean hasNonMatchFrame = mainWindowFrame != null;
+            final AppCompatTaskInfo compatInfo = taskInfo.appCompatTaskInfo;
+            final boolean isSizeCompatOrLetterboxed = compatInfo.isTopActivityInSizeCompat()
+                    || compatInfo.isTopActivityLetterboxed();
+            // For the animation to swipe PIP to home or restore a PIP task from home, we don't
+            // override to the main window frame since we should animate the whole task.
+            final boolean shouldUseMainWindowFrame = mainWindowFrame != null
+                    && !alwaysAnimateTaskBounds && !isSizeCompatOrLetterboxed;
             final boolean changeOrientation =
                     rotationDelta == ROTATION_90 || rotationDelta == ROTATION_270;
             final Rect baseBounds = new Rect(baseValue);
             final Rect startBounds = new Rect(startValue);
             final Rect endBounds = new Rect(endValue);
             if (isOutPipDirection) {
-                // TODO(b/356277166): handle rotation change with activity that provides main window
-                //  frame.
-                if (hasNonMatchFrame && !changeOrientation) {
+                if (shouldUseMainWindowFrame && !changeOrientation) {
                     endBounds.set(mainWindowFrame);
                 }
                 initialSourceValue = new Rect(endBounds);
             } else if (isInPipDirection) {
-                if (hasNonMatchFrame) {
+                if (shouldUseMainWindowFrame) {
                     baseBounds.set(mainWindowFrame);
                     if (startValue.equals(baseValue)) {
                         // If the start value is at initial state as in PIP animation, also override
@@ -635,9 +641,19 @@
             if (changeOrientation) {
                 lastEndRect = new Rect(endBounds);
                 rotatedEndRect = new Rect(endBounds);
-                // Rotate the end bounds according to the rotation delta because the display will
-                // be rotated to the same orientation.
-                rotateBounds(rotatedEndRect, initialSourceValue, rotationDelta);
+                // TODO(b/375977163): polish the animation to restoring the PIP task back from
+                //  swipe-pip-to-home. Ideally we should send the transitionInfo after reparenting
+                //  the PIP activity back to the original task.
+                if (shouldUseMainWindowFrame) {
+                    // If we should animate the main window frame, set it to the rotatedRect
+                    // instead. The end bounds reported by transitionInfo is the bounds before
+                    // rotation, while main window frame is calculated after the rotation.
+                    rotatedEndRect.set(mainWindowFrame);
+                } else {
+                    // Rotate the end bounds according to the rotation delta because the display
+                    // will be rotated to the same orientation.
+                    rotateBounds(rotatedEndRect, initialSourceValue, rotationDelta);
+                }
                 // Use the rect that has the same orientation as the hint rect.
                 initialContainerRect = isOutPipDirection ? rotatedEndRect : initialSourceValue;
             } else {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index 86c826a..30f1948 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -1880,9 +1880,11 @@
                 ? mPipBoundsState.getBounds() : currentBounds;
         final boolean existingAnimatorRunning = mPipAnimationController.getCurrentAnimator() != null
                 && mPipAnimationController.getCurrentAnimator().isRunning();
+        // For resize animation, we always animate the whole PIP task bounds.
         final PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController
                 .getAnimator(mTaskInfo, mLeash, baseBounds, currentBounds, destinationBounds,
-                        sourceHintRect, direction, startingAngle, rotationDelta);
+                        sourceHintRect, direction, startingAngle, rotationDelta,
+                        true /* alwaysAnimateTaskBounds */);
         animator.setTransitionDirection(direction)
                 .setPipTransactionHandler(mPipTransactionHandler)
                 .setDuration(durationMs);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 8220ea5..f7aed44 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -891,7 +891,8 @@
         final PipAnimationController.PipTransitionAnimator animator =
                 mPipAnimationController.getAnimator(taskInfo, pipChange.getLeash(),
                         startBounds, startBounds, endBounds, null, TRANSITION_DIRECTION_LEAVE_PIP,
-                        0 /* startingAngle */, pipRotateDelta);
+                        0 /* startingAngle */, pipRotateDelta,
+                        false /* alwaysAnimateTaskBounds */);
         animator.setTransitionDirection(TRANSITION_DIRECTION_LEAVE_PIP)
                 .setPipAnimationCallback(mPipAnimationCallback)
                 .setDuration(mEnterExitAnimationDuration)
@@ -906,7 +907,7 @@
         final PipAnimationController.PipTransitionAnimator animator =
                 mPipAnimationController.getAnimator(taskInfo, leash, baseBounds, startBounds,
                         endBounds, sourceHintRect, TRANSITION_DIRECTION_LEAVE_PIP,
-                        0 /* startingAngle */, rotationDelta);
+                        0 /* startingAngle */, rotationDelta, false /* alwaysAnimateTaskBounds */);
         animator.setTransitionDirection(TRANSITION_DIRECTION_LEAVE_PIP)
                 .setDuration(mEnterExitAnimationDuration);
         if (startTransaction != null) {
@@ -1102,8 +1103,6 @@
         if (taskInfo.pictureInPictureParams != null
                 && taskInfo.pictureInPictureParams.isAutoEnterEnabled()
                 && mPipTransitionState.getInSwipePipToHomeTransition()) {
-            // TODO(b/356277166): add support to swipe PIP to home with
-            //  non-match parent activity.
             handleSwipePipToHomeTransition(startTransaction, finishTransaction, leash,
                     sourceHintRect, destinationBounds, taskInfo);
             return;
@@ -1125,7 +1124,7 @@
         if (enterAnimationType == ANIM_TYPE_BOUNDS) {
             animator = mPipAnimationController.getAnimator(taskInfo, leash, currentBounds,
                     currentBounds, destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP,
-                    0 /* startingAngle */, rotationDelta);
+                    0 /* startingAngle */, rotationDelta, false /* alwaysAnimateTaskBounds */);
             if (sourceHintRect == null) {
                 // We use content overlay when there is no source rect hint to enter PiP use bounds
                 // animation. We also temporarily disallow app icon overlay and use color overlay
@@ -1248,10 +1247,14 @@
         // to avoid flicker.
         final Rect savedDisplayCutoutInsets = new Rect(pipTaskInfo.displayCutoutInsets);
         pipTaskInfo.displayCutoutInsets.setEmpty();
+        // Always use the task bounds even if the PIP activity doesn't match parent because the app
+        // and the whole task will move behind. We should animate the whole task bounds in this
+        // case.
         final PipAnimationController.PipTransitionAnimator animator =
                 mPipAnimationController.getAnimator(pipTaskInfo, leash, sourceBounds, sourceBounds,
                         destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP,
-                        0 /* startingAngle */, ROTATION_0 /* rotationDelta */)
+                        0 /* startingAngle */, ROTATION_0 /* rotationDelta */,
+                        true /* alwaysAnimateTaskBounds */)
                         .setPipTransactionHandler(mTransactionConsumer)
                         .setTransitionDirection(TRANSITION_DIRECTION_TO_PIP);
         // The start state is the end state for swipe-auto-pip.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 19a73f3..cc0e1df 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -55,6 +55,7 @@
 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE;
 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
 import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_LAUNCHER;
+import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_MULTI_INSTANCE;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_FINISHED;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP;
@@ -1098,11 +1099,16 @@
 
     void setSideStagePosition(@SplitPosition int sideStagePosition,
             @Nullable WindowContainerTransaction wct) {
+        setSideStagePosition(sideStagePosition, true /* updateBounds */, wct);
+    }
+
+    private void setSideStagePosition(@SplitPosition int sideStagePosition, boolean updateBounds,
+            @Nullable WindowContainerTransaction wct) {
         if (mSideStagePosition == sideStagePosition) return;
         mSideStagePosition = sideStagePosition;
         sendOnStagePositionChanged();
 
-        if (mSideStage.mVisible) {
+        if (mSideStage.mVisible && updateBounds) {
             if (wct == null) {
                 // onLayoutChanged builds/applies a wct with the contents of updateWindowBounds.
                 onLayoutSizeChanged(mSplitLayout);
@@ -1193,7 +1199,6 @@
         if (!isSplitActive()) return;
 
         final WindowContainerTransaction wct = new WindowContainerTransaction();
-        setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, wct);
         applyExitSplitScreen(childrenToTop, wct, exitReason);
     }
 
@@ -1593,13 +1598,6 @@
         }
         if (present) {
             updateRecentTasksSplitPair();
-        } else if (mMainStage.getChildCount() == 0 && mSideStage.getChildCount() == 0) {
-            mRecentTasks.ifPresent(recentTasks -> {
-                // remove the split pair mapping from recentTasks, and disable further updates
-                // to splits in the recents until we enter split again.
-                recentTasks.removeSplitPair(taskId);
-            });
-            exitSplitScreen(mMainStage, EXIT_REASON_ROOT_TASK_VANISHED);
         }
 
         for (int i = mListeners.size() - 1; i >= 0; --i) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 29e4b5b..9fcf98b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -353,7 +353,7 @@
             boolean isSeamlessDisplayChange = false;
 
             if (mode == TRANSIT_CHANGE && change.hasFlags(FLAG_IS_DISPLAY)) {
-                if (info.getType() == TRANSIT_CHANGE) {
+                if (info.getType() == TRANSIT_CHANGE || isOnlyTranslucent) {
                     final int anim = getRotationAnimationHint(change, info, mDisplayController);
                     isSeamlessDisplayChange = anim == ROTATION_ANIMATION_SEAMLESS;
                     if (!(isSeamlessDisplayChange || anim == ROTATION_ANIMATION_JUMPCUT)) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
index 7265fb8..c9f2d2e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -26,6 +26,7 @@
 
 import static com.android.window.flags.Flags.enableDisplayFocusInShellTransitions;
 
+import android.annotation.NonNull;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -110,6 +111,7 @@
                     }
                     mMainExecutor.execute(() -> {
                         mExclusionRegion.set(systemGestureExclusion);
+                        onExclusionRegionChanged(displayId, mExclusionRegion);
                     });
                 }
             };
@@ -163,7 +165,7 @@
             boolean isFocusedGlobally) {
         final WindowDecoration decor = mWindowDecorByTaskId.get(taskId);
         if (decor != null) {
-            decor.relayout(decor.mTaskInfo, isFocusedGlobally);
+            decor.relayout(decor.mTaskInfo, isFocusedGlobally, decor.mExclusionRegion);
         }
     }
 
@@ -199,9 +201,9 @@
 
         if (enableDisplayFocusInShellTransitions()) {
             // Pass the current global focus status to avoid updates outside of a ShellTransition.
-            decoration.relayout(taskInfo, decoration.mHasGlobalFocus);
+            decoration.relayout(taskInfo, decoration.mHasGlobalFocus, decoration.mExclusionRegion);
         } else {
-            decoration.relayout(taskInfo, taskInfo.isFocused);
+            decoration.relayout(taskInfo, taskInfo.isFocused, decoration.mExclusionRegion);
         }
     }
 
@@ -240,7 +242,7 @@
         } else {
             decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */,
                     false /* setTaskCropAndPosition */,
-                    mFocusTransitionObserver.hasGlobalFocus(taskInfo));
+                    mFocusTransitionObserver.hasGlobalFocus(taskInfo), mExclusionRegion);
         }
     }
 
@@ -254,7 +256,7 @@
 
         decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */,
                 false /* setTaskCropAndPosition */,
-                mFocusTransitionObserver.hasGlobalFocus(taskInfo));
+                mFocusTransitionObserver.hasGlobalFocus(taskInfo), mExclusionRegion);
     }
 
     @Override
@@ -266,6 +268,15 @@
         decoration.close();
     }
 
+    private void onExclusionRegionChanged(int displayId, @NonNull Region exclusionRegion) {
+        final int decorCount = mWindowDecorByTaskId.size();
+        for (int i = 0; i < decorCount; i++) {
+            final CaptionWindowDecoration decoration = mWindowDecorByTaskId.valueAt(i);
+            if (decoration.mTaskInfo.displayId != displayId) continue;
+            decoration.onExclusionRegionChanged(exclusionRegion);
+        }
+    }
+
     private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) {
         if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
             return true;
@@ -333,7 +344,7 @@
         windowDecoration.setTaskDragResizer(taskPositioner);
         windowDecoration.relayout(taskInfo, startT, finishT,
                 false /* applyStartTransactionOnDraw */, false /* setTaskCropAndPosition */,
-                mFocusTransitionObserver.hasGlobalFocus(taskInfo));
+                mFocusTransitionObserver.hasGlobalFocus(taskInfo), mExclusionRegion);
     }
 
     private class CaptionTouchEventListener implements
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index c954673..982fda0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -36,6 +36,7 @@
 import android.graphics.Insets;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.graphics.Region;
 import android.graphics.drawable.GradientDrawable;
 import android.os.Handler;
 import android.util.Size;
@@ -174,7 +175,8 @@
     }
 
     @Override
-    void relayout(RunningTaskInfo taskInfo, boolean hasGlobalFocus) {
+    void relayout(RunningTaskInfo taskInfo, boolean hasGlobalFocus,
+            @NonNull Region displayExclusionRegion) {
         final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
         // The crop and position of the task should only be set when a task is fluid resizing. In
         // all other cases, it is expected that the transition handler positions and crops the task
@@ -186,7 +188,7 @@
         // synced with the buffer transaction (that draws the View). Both will be shown on screen
         // at the same, whereas applying them independently causes flickering. See b/270202228.
         relayout(taskInfo, t, t, true /* applyStartTransactionOnDraw */,
-                shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus);
+                shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus, displayExclusionRegion);
     }
 
     @VisibleForTesting
@@ -198,7 +200,8 @@
             boolean isStatusBarVisible,
             boolean isKeyguardVisibleAndOccluded,
             InsetsState displayInsetsState,
-            boolean hasGlobalFocus) {
+            boolean hasGlobalFocus,
+            @NonNull Region globalExclusionRegion) {
         relayoutParams.reset();
         relayoutParams.mRunningTaskInfo = taskInfo;
         relayoutParams.mLayoutResId = R.layout.caption_window_decor;
@@ -210,6 +213,7 @@
         relayoutParams.mSetTaskVisibilityPositionAndCrop = shouldSetTaskVisibilityPositionAndCrop;
         relayoutParams.mIsCaptionVisible = taskInfo.isFreeform()
                 || (isStatusBarVisible && !isKeyguardVisibleAndOccluded);
+        relayoutParams.mDisplayExclusionRegion.set(globalExclusionRegion);
 
         if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) {
             // If the app is requesting to customize the caption bar, allow input to fall
@@ -236,7 +240,8 @@
     void relayout(RunningTaskInfo taskInfo,
             SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
             boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop,
-            boolean hasGlobalFocus) {
+            boolean hasGlobalFocus,
+            @NonNull Region globalExclusionRegion) {
         final boolean isFreeform =
                 taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM;
         final boolean isDragResizeable = ENABLE_WINDOWING_SCALED_RESIZING.isTrue()
@@ -249,7 +254,8 @@
         updateRelayoutParams(mRelayoutParams, taskInfo, applyStartTransactionOnDraw,
                 shouldSetTaskVisibilityPositionAndCrop, mIsStatusBarVisible,
                 mIsKeyguardVisibleAndOccluded,
-                mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus);
+                mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus,
+                globalExclusionRegion);
 
         relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult);
         // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 5e3026a..d71e61a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -220,6 +220,7 @@
                     }
                     mMainExecutor.execute(() -> {
                         mExclusionRegion.set(systemGestureExclusion);
+                        onExclusionRegionChanged(displayId, mExclusionRegion);
                     });
                 }
             };
@@ -432,7 +433,7 @@
             boolean isFocusedGlobally) {
         final WindowDecoration decor = mWindowDecorByTaskId.get(taskId);
         if (decor != null) {
-            decor.relayout(decor.mTaskInfo, isFocusedGlobally);
+            decor.relayout(decor.mTaskInfo, isFocusedGlobally, decor.mExclusionRegion);
         }
     }
 
@@ -471,9 +472,9 @@
         }
         if (enableDisplayFocusInShellTransitions()) {
             // Pass the current global focus status to avoid updates outside of a ShellTransition.
-            decoration.relayout(taskInfo, decoration.mHasGlobalFocus);
+            decoration.relayout(taskInfo, decoration.mHasGlobalFocus, decoration.mExclusionRegion);
         } else {
-            decoration.relayout(taskInfo, taskInfo.isFocused);
+            decoration.relayout(taskInfo, taskInfo.isFocused, decoration.mExclusionRegion);
         }
         mActivityOrientationChangeHandler.ifPresent(handler ->
                 handler.handleActivityOrientationChange(oldTaskInfo, taskInfo));
@@ -514,7 +515,8 @@
         } else {
             decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */,
                     false /* shouldSetTaskPositionAndCrop */,
-                    mFocusTransitionObserver.hasGlobalFocus(taskInfo));
+                    mFocusTransitionObserver.hasGlobalFocus(taskInfo),
+                    mExclusionRegion);
         }
     }
 
@@ -528,7 +530,8 @@
 
         decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */,
                 false /* shouldSetTaskPositionAndCrop */,
-                mFocusTransitionObserver.hasGlobalFocus(taskInfo));
+                mFocusTransitionObserver.hasGlobalFocus(taskInfo),
+                mExclusionRegion);
     }
 
     @Override
@@ -548,6 +551,15 @@
         mWindowDecorByTaskId.remove(taskInfo.taskId);
     }
 
+    private void onExclusionRegionChanged(int displayId, @NonNull Region exclusionRegion) {
+        final int decorCount = mWindowDecorByTaskId.size();
+        for (int i = 0; i < decorCount; i++) {
+            final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.valueAt(i);
+            if (decoration.mTaskInfo.displayId != displayId) continue;
+            decoration.onExclusionRegionChanged(exclusionRegion);
+        }
+    }
+
     private void openHandleMenu(int taskId) {
         final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
         decoration.createHandleMenu(checkNumberOfOtherInstances(decoration.mTaskInfo)
@@ -750,10 +762,13 @@
 
         /**
          * Whether to pilfer the next motion event to send cancellations to the windows below.
-         * Useful when the caption window is spy and the gesture should be handle by the system
+         * Useful when the caption window is spy and the gesture should be handled by the system
          * instead of by the app for their custom header content.
+         * Should not have any effect when {@link Flags#enableAccessibleCustomHeaders()}, because
+         * a spy window is not used then.
          */
-        private boolean mShouldPilferCaptionEvents;
+        private boolean mIsCustomHeaderGesture;
+        private boolean mIsResizeGesture;
         private boolean mIsDragging;
         private boolean mTouchscreenInUse;
         private boolean mHasLongClicked;
@@ -767,7 +782,8 @@
             mTaskToken = taskInfo.token;
             mDragPositioningCallback = dragPositioningCallback;
             final int touchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
-            final long appHandleHoldToDragDuration = Flags.enableHoldToDragAppHandle()
+            final long appHandleHoldToDragDuration =
+                    DesktopModeFlags.ENABLE_HOLD_TO_DRAG_APP_HANDLE.isTrue()
                     ? APP_HANDLE_HOLD_TO_DRAG_DURATION_MS : 0;
             mHandleDragDetector = new DragDetector(this, appHandleHoldToDragDuration,
                     touchSlop);
@@ -867,7 +883,7 @@
                 // to offset position relative to caption as a whole.
                 int[] viewLocation = new int[2];
                 v.getLocationInWindow(viewLocation);
-                final boolean isResizeEvent = decoration.shouldResizeListenerHandleEvent(e,
+                mIsResizeGesture = decoration.shouldResizeListenerHandleEvent(e,
                         new Point(viewLocation[0], viewLocation[1]));
                 // The caption window may be a spy window when the caption background is
                 // transparent, which means events will fall through to the app window. Make
@@ -875,21 +891,23 @@
                 // customizable region and what the app reported as exclusion areas, because
                 // the drag-move or other caption gestures should take priority outside those
                 // regions.
-                mShouldPilferCaptionEvents = !(downInCustomizableCaptionRegion
-                        && downInExclusionRegion && isTransparentCaption) && !isResizeEvent;
+                mIsCustomHeaderGesture = downInCustomizableCaptionRegion
+                        && downInExclusionRegion && isTransparentCaption;
             }
-            if (!mShouldPilferCaptionEvents) {
-                // The event will be handled by a window below or pilfered by resize handler.
+            if (mIsCustomHeaderGesture || mIsResizeGesture) {
+                // The event will be handled by the custom window below or pilfered by resize
+                // handler.
                 return false;
             }
-            // Otherwise pilfer so that windows below receive cancellations for this gesture, and
-            // continue normal handling as a caption gesture.
-            if (mInputManager != null) {
+            if (mInputManager != null
+                    && !Flags.enableAccessibleCustomHeaders()) {
+                // Pilfer so that windows below receive cancellations for this gesture.
                 mInputManager.pilferPointers(v.getViewRootImpl().getInputToken());
             }
             if (isUpOrCancel) {
                 // Gesture is finished, reset state.
-                mShouldPilferCaptionEvents = false;
+                mIsCustomHeaderGesture = false;
+                mIsResizeGesture = false;
             }
             if (isAppHandle) {
                 return mHandleDragDetector.onMotionEvent(v, e);
@@ -1592,7 +1610,8 @@
         windowDecoration.setDragPositioningCallback(taskPositioner);
         windowDecoration.relayout(taskInfo, startT, finishT,
                 false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */,
-                mFocusTransitionObserver.hasGlobalFocus(taskInfo));
+                mFocusTransitionObserver.hasGlobalFocus(taskInfo),
+                mExclusionRegion);
         if (!DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) {
             incrementEventReceiverTasks(taskInfo.displayId);
         }
@@ -1618,6 +1637,7 @@
         pw.println(innerPrefix + "mTransitionDragActive=" + mTransitionDragActive);
         pw.println(innerPrefix + "mEventReceiversByDisplay=" + mEventReceiversByDisplay);
         pw.println(innerPrefix + "mWindowDecorByTaskId=" + mWindowDecorByTaskId);
+        pw.println(innerPrefix + "mExclusionRegion=" + mExclusionRegion);
     }
 
     private class DesktopModeOnTaskRepositionAnimationListener
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index d377232..cdcf14e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -394,7 +394,8 @@
     }
 
     @Override
-    void relayout(ActivityManager.RunningTaskInfo taskInfo, boolean hasGlobalFocus) {
+    void relayout(ActivityManager.RunningTaskInfo taskInfo, boolean hasGlobalFocus,
+            @NonNull Region displayExclusionRegion) {
         final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
         // The visibility, crop and position of the task should only be set when a task is
         // fluid resizing. In all other cases, it is expected that the transition handler sets
@@ -415,7 +416,7 @@
         // causes flickering. See b/270202228.
         final boolean applyTransactionOnDraw = taskInfo.isFreeform();
         relayout(taskInfo, t, t, applyTransactionOnDraw, shouldSetTaskVisibilityPositionAndCrop,
-                hasGlobalFocus);
+                hasGlobalFocus, displayExclusionRegion);
         if (!applyTransactionOnDraw) {
             t.apply();
         }
@@ -442,18 +443,18 @@
     void relayout(ActivityManager.RunningTaskInfo taskInfo,
             SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
             boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop,
-            boolean hasGlobalFocus) {
+            boolean hasGlobalFocus, @NonNull Region displayExclusionRegion) {
         Trace.beginSection("DesktopModeWindowDecoration#relayout");
         if (taskInfo.isFreeform()) {
             // The Task is in Freeform mode -> show its header in sync since it's an integral part
             // of the window itself - a delayed header might cause bad UX.
             relayoutInSync(taskInfo, startT, finishT, applyStartTransactionOnDraw,
-                    shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus);
+                    shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus, displayExclusionRegion);
         } else {
             // The Task is outside Freeform mode -> allow the handle view to be delayed since the
             // handle is just a small addition to the window.
             relayoutWithDelayedViewHost(taskInfo, startT, finishT, applyStartTransactionOnDraw,
-                    shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus);
+                    shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus, displayExclusionRegion);
         }
         Trace.endSection();
     }
@@ -462,11 +463,11 @@
     private void relayoutInSync(ActivityManager.RunningTaskInfo taskInfo,
             SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
             boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop,
-            boolean hasGlobalFocus) {
+            boolean hasGlobalFocus, @NonNull Region displayExclusionRegion) {
         // Clear the current ViewHost runnable as we will update the ViewHost here
         clearCurrentViewHostRunnable();
         updateRelayoutParamsAndSurfaces(taskInfo, startT, finishT, applyStartTransactionOnDraw,
-                shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus);
+                shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus, displayExclusionRegion);
         if (mResult.mRootView != null) {
             updateViewHost(mRelayoutParams, startT, mResult);
         }
@@ -489,7 +490,8 @@
     private void relayoutWithDelayedViewHost(ActivityManager.RunningTaskInfo taskInfo,
             SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
             boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop,
-            boolean hasGlobalFocus) {
+            boolean hasGlobalFocus,
+            @NonNull Region displayExclusionRegion) {
         if (applyStartTransactionOnDraw) {
             throw new IllegalArgumentException(
                     "We cannot both sync viewhost ondraw and delay viewhost creation.");
@@ -498,7 +500,7 @@
         clearCurrentViewHostRunnable();
         updateRelayoutParamsAndSurfaces(taskInfo, startT, finishT,
                 false /* applyStartTransactionOnDraw */, shouldSetTaskVisibilityPositionAndCrop,
-                hasGlobalFocus);
+                hasGlobalFocus, displayExclusionRegion);
         if (mResult.mRootView == null) {
             // This means something blocks the window decor from showing, e.g. the task is hidden.
             // Nothing is set up in this case including the decoration surface.
@@ -513,7 +515,7 @@
     private void updateRelayoutParamsAndSurfaces(ActivityManager.RunningTaskInfo taskInfo,
             SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
             boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop,
-            boolean hasGlobalFocus) {
+            boolean hasGlobalFocus, @NonNull Region displayExclusionRegion) {
         Trace.beginSection("DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces");
         if (Flags.enableDesktopWindowingAppToWeb()) {
             setCapturedLink(taskInfo.capturedLink, taskInfo.capturedLinkTimestamp);
@@ -538,7 +540,8 @@
         updateRelayoutParams(mRelayoutParams, mContext, taskInfo, applyStartTransactionOnDraw,
                 shouldSetTaskVisibilityPositionAndCrop, mIsStatusBarVisible,
                 mIsKeyguardVisibleAndOccluded, inFullImmersive,
-                mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus);
+                mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus,
+                displayExclusionRegion);
 
         final WindowDecorLinearLayout oldRootView = mResult.mRootView;
         final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
@@ -628,13 +631,6 @@
 
     @Nullable
     private Intent getBrowserLink() {
-        // Do not show browser link in browser applications
-        final ComponentName baseActivity = mTaskInfo.baseActivity;
-        if (baseActivity != null && AppToWebUtils.isBrowserApp(mContext,
-                baseActivity.getPackageName(), mUserContext.getUserId())) {
-            return null;
-        }
-
         final Uri browserLink;
         // If the captured link is available and has not expired, return the captured link.
         // Otherwise, return the generic link which is set to null if a generic link is unavailable.
@@ -651,6 +647,18 @@
 
     }
 
+    @Nullable
+    private Intent getAppLink() {
+        return mWebUri == null ? null
+                : AppToWebUtils.getAppIntent(mWebUri, mContext.getPackageManager());
+    }
+
+    private boolean isBrowserApp() {
+        final ComponentName baseActivity = mTaskInfo.baseActivity;
+        return baseActivity != null && AppToWebUtils.isBrowserApp(mContext,
+                baseActivity.getPackageName(), mUserContext.getUserId());
+    }
+
     UserHandle getUser() {
         return mUserContext.getUser();
     }
@@ -874,7 +882,8 @@
             boolean isKeyguardVisibleAndOccluded,
             boolean inFullImmersiveMode,
             @NonNull InsetsState displayInsetsState,
-            boolean hasGlobalFocus) {
+            boolean hasGlobalFocus,
+            @NonNull Region displayExclusionRegion) {
         final int captionLayoutId = getDesktopModeWindowDecorLayoutId(taskInfo.getWindowingMode());
         final boolean isAppHeader =
                 captionLayoutId == R.layout.desktop_mode_app_header;
@@ -885,6 +894,7 @@
         relayoutParams.mCaptionHeightId = getCaptionHeightIdStatic(taskInfo.getWindowingMode());
         relayoutParams.mCaptionWidthId = getCaptionWidthId(relayoutParams.mLayoutResId);
         relayoutParams.mHasGlobalFocus = hasGlobalFocus;
+        relayoutParams.mDisplayExclusionRegion.set(displayExclusionRegion);
 
         final boolean showCaption;
         if (Flags.enableFullyImmersiveInDesktop()) {
@@ -910,10 +920,20 @@
         relayoutParams.mIsInsetSource = isAppHeader && !inFullImmersiveMode;
         if (isAppHeader) {
             if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) {
-                // If the app is requesting to customize the caption bar, allow input to fall
-                // through to the windows below so that the app can respond to input events on
-                // their custom content.
-                relayoutParams.mInputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_SPY;
+                // The app is requesting to customize the caption bar, which means input on
+                // customizable/exclusion regions must go to the app instead of to the system.
+                // This may be accomplished with spy windows or custom touchable regions:
+                if (Flags.enableAccessibleCustomHeaders()) {
+                    // Set the touchable region of the caption to only the areas where input should
+                    // be handled by the system (i.e. non custom-excluded areas). The region will
+                    // be calculated based on occluding caption elements and exclusion areas
+                    // reported by the app.
+                    relayoutParams.mLimitTouchRegionToSystemAreas = true;
+                } else {
+                    // Allow input to fall through to the windows below so that the app can respond
+                    // to input events on their custom content.
+                    relayoutParams.mInputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_SPY;
+                }
             } else {
                 if (ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION.isTrue()) {
                     // Force-consume the caption bar insets when the app tries to hide the caption.
@@ -1368,6 +1388,7 @@
                 .shouldShowChangeAspectRatioButton(mTaskInfo);
         final boolean inDesktopImmersive = mDesktopRepository
                 .isTaskInFullImmersiveState(mTaskInfo.taskId);
+        final boolean isBrowserApp = isBrowserApp();
         mHandleMenu = mHandleMenuFactory.create(
                 this,
                 mWindowManagerWrapper,
@@ -1379,7 +1400,8 @@
                 supportsMultiInstance,
                 shouldShowManageWindowsButton,
                 shouldShowChangeAspectRatioButton,
-                getBrowserLink(),
+                isBrowserApp,
+                isBrowserApp ? getAppLink() : getBrowserLink(),
                 mResult.mCaptionWidth,
                 mResult.mCaptionHeight,
                 mResult.mCaptionX,
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 2da20bd..54c247b 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
@@ -41,10 +41,12 @@
 import android.widget.TextView
 import android.window.DesktopModeFlags
 import android.window.SurfaceSyncGroup
+import androidx.annotation.StringRes
 import androidx.annotation.VisibleForTesting
 import androidx.compose.ui.graphics.toArgb
 import androidx.core.view.isGone
 import com.android.wm.shell.R
+import com.android.wm.shell.apptoweb.isBrowserApp
 import com.android.wm.shell.shared.split.SplitScreenConstants
 import com.android.wm.shell.splitscreen.SplitScreenController
 import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
@@ -73,7 +75,8 @@
     private val shouldShowNewWindowButton: Boolean,
     private val shouldShowManageWindowsButton: Boolean,
     private val shouldShowChangeAspectRatioButton: Boolean,
-    private val openInBrowserIntent: Intent?,
+    private val isBrowserApp: Boolean,
+    private val openInAppOrBrowserIntent: Intent?,
     private val captionWidth: Int,
     private val captionHeight: Int,
     captionX: Int,
@@ -111,7 +114,7 @@
     private val globalMenuPosition: Point = Point()
 
     private val shouldShowBrowserPill: Boolean
-        get() = openInBrowserIntent != null
+        get() = openInAppOrBrowserIntent != null
 
     private val shouldShowMoreActionsPill: Boolean
         get() = SHOULD_SHOW_SCREENSHOT_BUTTON || shouldShowNewWindowButton ||
@@ -128,7 +131,7 @@
         onNewWindowClickListener: () -> Unit,
         onManageWindowsClickListener: () -> Unit,
         onChangeAspectRatioClickListener: () -> Unit,
-        openInBrowserClickListener: (Intent) -> Unit,
+        openInAppOrBrowserClickListener: (Intent) -> Unit,
         onOpenByDefaultClickListener: () -> Unit,
         onCloseMenuClickListener: () -> Unit,
         onOutsideTouchListener: () -> Unit,
@@ -146,7 +149,7 @@
             onNewWindowClickListener = onNewWindowClickListener,
             onManageWindowsClickListener = onManageWindowsClickListener,
             onChangeAspectRatioClickListener = onChangeAspectRatioClickListener,
-            openInBrowserClickListener = openInBrowserClickListener,
+            openInAppOrBrowserClickListener = openInAppOrBrowserClickListener,
             onOpenByDefaultClickListener = onOpenByDefaultClickListener,
             onCloseMenuClickListener = onCloseMenuClickListener,
             onOutsideTouchListener = onOutsideTouchListener,
@@ -167,7 +170,7 @@
         onNewWindowClickListener: () -> Unit,
         onManageWindowsClickListener: () -> Unit,
         onChangeAspectRatioClickListener: () -> Unit,
-        openInBrowserClickListener: (Intent) -> Unit,
+        openInAppOrBrowserClickListener: (Intent) -> Unit,
         onOpenByDefaultClickListener: () -> Unit,
         onCloseMenuClickListener: () -> Unit,
         onOutsideTouchListener: () -> Unit,
@@ -181,7 +184,8 @@
             shouldShowBrowserPill = shouldShowBrowserPill,
             shouldShowNewWindowButton = shouldShowNewWindowButton,
             shouldShowManageWindowsButton = shouldShowManageWindowsButton,
-            shouldShowChangeAspectRatioButton = shouldShowChangeAspectRatioButton
+            shouldShowChangeAspectRatioButton = shouldShowChangeAspectRatioButton,
+            isBrowserApp = isBrowserApp
         ).apply {
             bind(taskInfo, appIconBitmap, appName, shouldShowMoreActionsPill)
             this.onToDesktopClickListener = onToDesktopClickListener
@@ -190,8 +194,8 @@
             this.onNewWindowClickListener = onNewWindowClickListener
             this.onManageWindowsClickListener = onManageWindowsClickListener
             this.onChangeAspectRatioClickListener = onChangeAspectRatioClickListener
-            this.onOpenInBrowserClickListener = {
-                openInBrowserClickListener.invoke(openInBrowserIntent!!)
+            this.onOpenInAppOrBrowserClickListener = {
+                openInAppOrBrowserClickListener.invoke(openInAppOrBrowserIntent!!)
             }
             this.onOpenByDefaultClickListener = onOpenByDefaultClickListener
             this.onCloseMenuClickListener = onCloseMenuClickListener
@@ -436,14 +440,15 @@
     /** The view within the Handle Menu, with options to change the windowing mode and more. */
     @SuppressLint("ClickableViewAccessibility")
     class HandleMenuView(
-        context: Context,
+        private val context: Context,
         menuWidth: Int,
         captionHeight: Int,
         private val shouldShowWindowingPill: Boolean,
         private val shouldShowBrowserPill: Boolean,
         private val shouldShowNewWindowButton: Boolean,
         private val shouldShowManageWindowsButton: Boolean,
-        private val shouldShowChangeAspectRatioButton: Boolean
+        private val shouldShowChangeAspectRatioButton: Boolean,
+        private val isBrowserApp: Boolean
     ) {
         val rootView = LayoutInflater.from(context)
             .inflate(R.layout.desktop_mode_window_decor_handle_menu, null /* root */) as View
@@ -473,11 +478,12 @@
         private val changeAspectRatioBtn = moreActionsPill
             .requireViewById<Button>(R.id.change_aspect_ratio_button)
 
-        // Open in Browser Pill.
-        private val openInBrowserPill = rootView.requireViewById<View>(R.id.open_in_browser_pill)
-        private val browserBtn = openInBrowserPill.requireViewById<Button>(
-            R.id.open_in_browser_button)
-        private val openByDefaultBtn = openInBrowserPill.requireViewById<ImageButton>(
+        // Open in Browser/App Pill.
+        private val openInAppOrBrowserPill = rootView.requireViewById<View>(
+            R.id.open_in_app_or_browser_pill)
+        private val openInAppOrBrowserBtn = openInAppOrBrowserPill.requireViewById<Button>(
+            R.id.open_in_app_or_browser_button)
+        private val openByDefaultBtn = openInAppOrBrowserPill.requireViewById<ImageButton>(
             R.id.open_by_default_button)
         private val decorThemeUtil = DecorThemeUtil(context)
         private val animator = HandleMenuAnimator(rootView, menuWidth, captionHeight.toFloat())
@@ -491,7 +497,7 @@
         var onNewWindowClickListener: (() -> Unit)? = null
         var onManageWindowsClickListener: (() -> Unit)? = null
         var onChangeAspectRatioClickListener: (() -> Unit)? = null
-        var onOpenInBrowserClickListener: (() -> Unit)? = null
+        var onOpenInAppOrBrowserClickListener: (() -> Unit)? = null
         var onOpenByDefaultClickListener: (() -> Unit)? = null
         var onCloseMenuClickListener: (() -> Unit)? = null
         var onOutsideTouchListener: (() -> Unit)? = null
@@ -500,7 +506,7 @@
             fullscreenBtn.setOnClickListener { onToFullscreenClickListener?.invoke() }
             splitscreenBtn.setOnClickListener { onToSplitScreenClickListener?.invoke() }
             desktopBtn.setOnClickListener { onToDesktopClickListener?.invoke() }
-            browserBtn.setOnClickListener { onOpenInBrowserClickListener?.invoke() }
+            openInAppOrBrowserBtn.setOnClickListener { onOpenInAppOrBrowserClickListener?.invoke() }
             openByDefaultBtn.setOnClickListener {
                 onOpenByDefaultClickListener?.invoke()
             }
@@ -536,10 +542,10 @@
             if (shouldShowMoreActionsPill) {
                 bindMoreActionsPill(style)
             }
-            bindOpenInBrowserPill(style)
+            bindOpenInAppOrBrowserPill(style)
         }
 
-        /** Animates the menu opening. */
+        /** Animates the menu openInAppOrBrowserg. */
         fun animateOpenMenu() {
             if (taskInfo.isFullscreen || taskInfo.isMultiWindow) {
                 animator.animateCaptionHandleExpandToOpen()
@@ -661,13 +667,20 @@
             }
         }
 
-        private fun bindOpenInBrowserPill(style: MenuStyle) {
-            openInBrowserPill.apply {
+        private fun bindOpenInAppOrBrowserPill(style: MenuStyle) {
+            openInAppOrBrowserPill.apply {
                 isGone = !shouldShowBrowserPill
                 background.setTint(style.backgroundColor)
             }
 
-            browserBtn.apply {
+            val btnText = if (isBrowserApp) {
+                getString(R.string.open_in_app_text)
+            } else {
+                getString(R.string.open_in_browser_text)
+            }
+            openInAppOrBrowserBtn.apply {
+                text = btnText
+                contentDescription = btnText
                 setTextColor(style.textColor)
                 compoundDrawableTintList = ColorStateList.valueOf(style.textColor)
             }
@@ -675,6 +688,8 @@
             openByDefaultBtn.imageTintList = ColorStateList.valueOf(style.textColor)
         }
 
+        private fun getString(@StringRes resId: Int): String = context.resources.getString(resId)
+
         private data class MenuStyle(
             @ColorInt val backgroundColor: Int,
             @ColorInt val textColor: Int,
@@ -709,7 +724,8 @@
         shouldShowNewWindowButton: Boolean,
         shouldShowManageWindowsButton: Boolean,
         shouldShowChangeAspectRatioButton: Boolean,
-        openInBrowserIntent: Intent?,
+        isBrowserApp: Boolean,
+        openInAppOrBrowserIntent: Intent?,
         captionWidth: Int,
         captionHeight: Int,
         captionX: Int,
@@ -730,7 +746,8 @@
         shouldShowNewWindowButton: Boolean,
         shouldShowManageWindowsButton: Boolean,
         shouldShowChangeAspectRatioButton: Boolean,
-        openInBrowserIntent: Intent?,
+        isBrowserApp: Boolean,
+        openInAppOrBrowserIntent: Intent?,
         captionWidth: Int,
         captionHeight: Int,
         captionX: Int,
@@ -747,7 +764,8 @@
             shouldShowNewWindowButton,
             shouldShowManageWindowsButton,
             shouldShowChangeAspectRatioButton,
-            openInBrowserIntent,
+            isBrowserApp,
+            openInAppOrBrowserIntent,
             captionWidth,
             captionHeight,
             captionX,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt
index 0c475f1..470e5a1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt
@@ -74,7 +74,8 @@
     private val appInfoPill: ViewGroup = handleMenu.requireViewById(R.id.app_info_pill)
     private val windowingPill: ViewGroup = handleMenu.requireViewById(R.id.windowing_pill)
     private val moreActionsPill: ViewGroup = handleMenu.requireViewById(R.id.more_actions_pill)
-    private val openInBrowserPill: ViewGroup = handleMenu.requireViewById(R.id.open_in_browser_pill)
+    private val openInAppOrBrowserPill: ViewGroup =
+        handleMenu.requireViewById(R.id.open_in_app_or_browser_pill)
 
     /** Animates the opening of the handle menu. */
     fun animateOpen() {
@@ -83,7 +84,7 @@
         animateAppInfoPillOpen()
         animateWindowingPillOpen()
         animateMoreActionsPillOpen()
-        animateOpenInBrowserPill()
+        animateOpenInAppOrBrowserPill()
         runAnimations {
             appInfoPill.post {
                 appInfoPill.requireViewById<View>(R.id.collapse_menu_button).sendAccessibilityEvent(
@@ -103,7 +104,7 @@
         animateAppInfoPillOpen()
         animateWindowingPillOpen()
         animateMoreActionsPillOpen()
-        animateOpenInBrowserPill()
+        animateOpenInAppOrBrowserPill()
         runAnimations {
             appInfoPill.post {
                 appInfoPill.requireViewById<View>(R.id.collapse_menu_button).sendAccessibilityEvent(
@@ -124,7 +125,7 @@
         animateAppInfoPillFadeOut()
         windowingPillClose()
         moreActionsPillClose()
-        openInBrowserPillClose()
+        openInAppOrBrowserPillClose()
         runAnimations(after)
     }
 
@@ -141,7 +142,7 @@
         animateAppInfoPillFadeOut()
         windowingPillClose()
         moreActionsPillClose()
-        openInBrowserPillClose()
+        openInAppOrBrowserPillClose()
         runAnimations(after)
     }
 
@@ -154,7 +155,7 @@
         appInfoPill.children.forEach { it.alpha = 0f }
         windowingPill.alpha = 0f
         moreActionsPill.alpha = 0f
-        openInBrowserPill.alpha = 0f
+        openInAppOrBrowserPill.alpha = 0f
 
         // Setup pivots.
         handleMenu.pivotX = menuWidth / 2f
@@ -166,8 +167,8 @@
         moreActionsPill.pivotX = menuWidth / 2f
         moreActionsPill.pivotY = appInfoPill.measuredHeight.toFloat()
 
-        openInBrowserPill.pivotX = menuWidth / 2f
-        openInBrowserPill.pivotY = appInfoPill.measuredHeight.toFloat()
+        openInAppOrBrowserPill.pivotX = menuWidth / 2f
+        openInAppOrBrowserPill.pivotY = appInfoPill.measuredHeight.toFloat()
     }
 
     private fun animateAppInfoPillOpen() {
@@ -297,36 +298,36 @@
         }
     }
 
-    private fun animateOpenInBrowserPill() {
+    private fun animateOpenInAppOrBrowserPill() {
         // Open in Browser X & Y Scaling Animation
         animators +=
-                ObjectAnimator.ofFloat(openInBrowserPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply {
+                ObjectAnimator.ofFloat(openInAppOrBrowserPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply {
                     startDelay = BODY_SCALE_OPEN_DELAY
                     duration = BODY_SCALE_OPEN_DURATION
                 }
 
         animators +=
-                ObjectAnimator.ofFloat(openInBrowserPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply {
+                ObjectAnimator.ofFloat(openInAppOrBrowserPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply {
                     startDelay = BODY_SCALE_OPEN_DELAY
                     duration = BODY_SCALE_OPEN_DURATION
                 }
 
         // Open in Browser Opacity Animation
         animators +=
-                ObjectAnimator.ofFloat(openInBrowserPill, ALPHA, 1f).apply {
+                ObjectAnimator.ofFloat(openInAppOrBrowserPill, ALPHA, 1f).apply {
                     startDelay = BODY_ALPHA_OPEN_DELAY
                     duration = BODY_ALPHA_OPEN_DURATION
                 }
 
         // Open in Browser Elevation Animation
         animators +=
-                ObjectAnimator.ofFloat(openInBrowserPill, TRANSLATION_Z, 1f).apply {
+                ObjectAnimator.ofFloat(openInAppOrBrowserPill, TRANSLATION_Z, 1f).apply {
                     startDelay = ELEVATION_OPEN_DELAY
                     duration = BODY_ELEVATION_OPEN_DURATION
                 }
 
         // Open in Browser Button Opacity Animation
-        val button = openInBrowserPill.requireViewById<Button>(R.id.open_in_browser_button)
+        val button = openInAppOrBrowserPill.requireViewById<Button>(R.id.open_in_app_or_browser_button)
         animators +=
                 ObjectAnimator.ofFloat(button, ALPHA, 1f).apply {
                     startDelay = BODY_ALPHA_OPEN_DELAY
@@ -438,33 +439,33 @@
             }
     }
 
-    private fun openInBrowserPillClose() {
+    private fun openInAppOrBrowserPillClose() {
         // Open in Browser X & Y Scaling Animation
         animators +=
-                ObjectAnimator.ofFloat(openInBrowserPill, SCALE_X, HALF_INITIAL_SCALE).apply {
+                ObjectAnimator.ofFloat(openInAppOrBrowserPill, SCALE_X, HALF_INITIAL_SCALE).apply {
                     duration = BODY_CLOSE_DURATION
                 }
 
         animators +=
-                ObjectAnimator.ofFloat(openInBrowserPill, SCALE_Y, HALF_INITIAL_SCALE).apply {
+                ObjectAnimator.ofFloat(openInAppOrBrowserPill, SCALE_Y, HALF_INITIAL_SCALE).apply {
                     duration = BODY_CLOSE_DURATION
                 }
 
         // Open in Browser Opacity Animation
         animators +=
-                ObjectAnimator.ofFloat(openInBrowserPill, ALPHA, 0f).apply {
+                ObjectAnimator.ofFloat(openInAppOrBrowserPill, ALPHA, 0f).apply {
                     duration = BODY_CLOSE_DURATION
                 }
 
         animators +=
-                ObjectAnimator.ofFloat(openInBrowserPill, ALPHA, 0f).apply {
+                ObjectAnimator.ofFloat(openInAppOrBrowserPill, ALPHA, 0f).apply {
                     duration = BODY_CLOSE_DURATION
                 }
 
         // Upward Open in Browser y-translation Animation
         val yStart: Float = -captionHeight / 2
         animators +=
-                ObjectAnimator.ofFloat(openInBrowserPill, TRANSLATION_Y, yStart).apply {
+                ObjectAnimator.ofFloat(openInAppOrBrowserPill, TRANSLATION_Y, yStart).apply {
                     duration = BODY_CLOSE_DURATION
                 }
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index b016c75..a3c75bf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -127,7 +127,7 @@
                     }
 
                     mDisplayController.removeDisplayWindowListener(this);
-                    relayout(mTaskInfo, mHasGlobalFocus);
+                    relayout(mTaskInfo, mHasGlobalFocus, mExclusionRegion);
                 }
             };
 
@@ -143,7 +143,7 @@
     SurfaceControl mDecorationContainerSurface;
 
     SurfaceControl mCaptionContainerSurface;
-    private WindowlessWindowManager mCaptionWindowManager;
+    private CaptionWindowlessWindowManager mCaptionWindowManager;
     private SurfaceControlViewHost mViewHost;
     private Configuration mWindowDecorConfig;
     TaskDragResizer mTaskDragResizer;
@@ -152,6 +152,7 @@
     boolean mIsStatusBarVisible;
     boolean mIsKeyguardVisibleAndOccluded;
     boolean mHasGlobalFocus;
+    final Region mExclusionRegion = Region.obtain();
 
     /** The most recent set of insets applied to this window decoration. */
     private WindowDecorationInsets mWindowDecorationInsets;
@@ -218,7 +219,8 @@
      *                 constructor.
      * @param hasGlobalFocus Whether the task is focused
      */
-    abstract void relayout(RunningTaskInfo taskInfo, boolean hasGlobalFocus);
+    abstract void relayout(RunningTaskInfo taskInfo, boolean hasGlobalFocus,
+            @NonNull Region displayExclusionRegion);
 
     /**
      * Used by the {@link DragPositioningCallback} associated with the implementing class to
@@ -244,6 +246,7 @@
             mTaskInfo = params.mRunningTaskInfo;
         }
         mHasGlobalFocus = params.mHasGlobalFocus;
+        mExclusionRegion.set(params.mDisplayExclusionRegion);
         final int oldLayoutResId = mLayoutResId;
         mLayoutResId = params.mLayoutResId;
 
@@ -402,7 +405,7 @@
                 final int elementWidthPx =
                         resources.getDimensionPixelSize(element.mWidthResId);
                 boundingRects[i] =
-                        calculateBoundingRect(element, elementWidthPx, captionInsetsRect);
+                        calculateBoundingRectLocal(element, elementWidthPx, captionInsetsRect);
                 // Subtract the regions used by the caption elements, the rest is
                 // customizable.
                 if (params.hasInputFeatureSpy()) {
@@ -477,9 +480,8 @@
         if (mCaptionWindowManager == null) {
             // Put caption under a container surface because ViewRootImpl sets the destination frame
             // of windowless window layers and BLASTBufferQueue#update() doesn't support offset.
-            mCaptionWindowManager = new WindowlessWindowManager(
-                    mTaskInfo.getConfiguration(), mCaptionContainerSurface,
-                    null /* hostInputToken */);
+            mCaptionWindowManager = new CaptionWindowlessWindowManager(
+                    mTaskInfo.getConfiguration(), mCaptionContainerSurface);
         }
         mCaptionWindowManager.setConfiguration(mTaskInfo.getConfiguration());
         final WindowManager.LayoutParams lp =
@@ -492,6 +494,14 @@
         lp.setTitle("Caption of Task=" + mTaskInfo.taskId);
         lp.setTrustedOverlay();
         lp.inputFeatures = params.mInputFeatures;
+        final Rect localCaptionBounds = new Rect(
+                outResult.mCaptionX,
+                outResult.mCaptionY,
+                outResult.mCaptionX + outResult.mCaptionWidth,
+                outResult.mCaptionY + outResult.mCaptionHeight);
+        final Region touchableRegion = params.mLimitTouchRegionToSystemAreas
+                ? calculateLimitedTouchableRegion(params, localCaptionBounds)
+                : null;
         if (mViewHost == null) {
             Trace.beginSection("CaptionViewHostLayout-new");
             mViewHost = mSurfaceControlViewHostFactory.create(mDecorWindowContext, mDisplay,
@@ -503,6 +513,9 @@
                 mViewHost.getRootSurfaceControl().applyTransactionOnDraw(onDrawTransaction);
             }
             outResult.mRootView.setPadding(0, params.mCaptionTopPadding, 0, 0);
+            if (params.mLimitTouchRegionToSystemAreas) {
+                mCaptionWindowManager.setTouchRegion(mViewHost, touchableRegion);
+            }
             mViewHost.setView(outResult.mRootView, lp);
             Trace.endSection();
         } else {
@@ -514,13 +527,71 @@
                 mViewHost.getRootSurfaceControl().applyTransactionOnDraw(onDrawTransaction);
             }
             outResult.mRootView.setPadding(0, params.mCaptionTopPadding, 0, 0);
+            if (params.mLimitTouchRegionToSystemAreas) {
+                mCaptionWindowManager.setTouchRegion(mViewHost, touchableRegion);
+            }
             mViewHost.relayout(lp);
             Trace.endSection();
         }
+        if (touchableRegion != null) {
+            touchableRegion.recycle();
+        }
         Trace.endSection(); // CaptionViewHostLayout
     }
 
-    private Rect calculateBoundingRect(@NonNull OccludingCaptionElement element,
+    @NonNull
+    private Region calculateLimitedTouchableRegion(
+            RelayoutParams params,
+            @NonNull Rect localCaptionBounds) {
+        // Make caption bounds relative to display to align with exclusion region.
+        final Point positionInParent = params.mRunningTaskInfo.positionInParent;
+        final Rect captionBoundsInDisplay = new Rect(localCaptionBounds);
+        captionBoundsInDisplay.offsetTo(positionInParent.x, positionInParent.y);
+
+        final Region boundingRects = calculateBoundingRectsRegion(params, captionBoundsInDisplay);
+
+        final Region customizedRegion = Region.obtain();
+        customizedRegion.set(captionBoundsInDisplay);
+        customizedRegion.op(boundingRects, Region.Op.DIFFERENCE);
+        customizedRegion.op(params.mDisplayExclusionRegion, Region.Op.INTERSECT);
+
+        final Region touchableRegion = Region.obtain();
+        touchableRegion.set(captionBoundsInDisplay);
+        touchableRegion.op(customizedRegion, Region.Op.DIFFERENCE);
+        // Return resulting region back to window coordinates.
+        touchableRegion.translate(-positionInParent.x, -positionInParent.y);
+
+        boundingRects.recycle();
+        customizedRegion.recycle();
+        return touchableRegion;
+    }
+
+    @NonNull
+    private Region calculateBoundingRectsRegion(
+            @NonNull RelayoutParams params,
+            @NonNull Rect captionBoundsInDisplay) {
+        final int numOfElements = params.mOccludingCaptionElements.size();
+        final Region region = Region.obtain();
+        if (numOfElements == 0) {
+            // The entire caption is a bounding rect.
+            region.set(captionBoundsInDisplay);
+            return region;
+        }
+        final Resources resources = mDecorWindowContext.getResources();
+        for (int i = 0; i < numOfElements; i++) {
+            final OccludingCaptionElement element = params.mOccludingCaptionElements.get(i);
+            final int elementWidthPx = resources.getDimensionPixelSize(element.mWidthResId);
+            final Rect boundingRect = calculateBoundingRectLocal(element, elementWidthPx,
+                    captionBoundsInDisplay);
+            // Bounding rect is initially calculated relative to the caption, so offset it to make
+            // it relative to the display.
+            boundingRect.offset(captionBoundsInDisplay.left, captionBoundsInDisplay.top);
+            region.union(boundingRect);
+        }
+        return region;
+    }
+
+    private Rect calculateBoundingRectLocal(@NonNull OccludingCaptionElement element,
             int elementWidthPx, @NonNull Rect captionRect) {
         switch (element.mAlignment) {
             case START -> {
@@ -539,7 +610,7 @@
         mIsKeyguardVisibleAndOccluded = visible && occluded;
         final boolean changed = prevVisAndOccluded != mIsKeyguardVisibleAndOccluded;
         if (changed) {
-            relayout(mTaskInfo, mHasGlobalFocus);
+            relayout(mTaskInfo, mHasGlobalFocus, mExclusionRegion);
         }
     }
 
@@ -549,10 +620,14 @@
         final boolean changed = prevStatusBarVisibility != mIsStatusBarVisible;
 
         if (changed) {
-            relayout(mTaskInfo, mHasGlobalFocus);
+            relayout(mTaskInfo, mHasGlobalFocus, mExclusionRegion);
         }
     }
 
+    void onExclusionRegionChanged(@NonNull Region exclusionRegion) {
+        relayout(mTaskInfo, mHasGlobalFocus, exclusionRegion);
+    }
+
     /**
      * Update caption visibility state and views.
      */
@@ -751,9 +826,11 @@
         int mCaptionHeightId;
         int mCaptionWidthId;
         final List<OccludingCaptionElement> mOccludingCaptionElements = new ArrayList<>();
+        boolean mLimitTouchRegionToSystemAreas;
         int mInputFeatures;
         boolean mIsInsetSource = true;
         @InsetsSource.Flags int mInsetSourceFlags;
+        final Region mDisplayExclusionRegion = Region.obtain();
 
         int mShadowRadiusId;
         int mCornerRadius;
@@ -772,9 +849,11 @@
             mCaptionHeightId = Resources.ID_NULL;
             mCaptionWidthId = Resources.ID_NULL;
             mOccludingCaptionElements.clear();
+            mLimitTouchRegionToSystemAreas = false;
             mInputFeatures = 0;
             mIsInsetSource = true;
             mInsetSourceFlags = 0;
+            mDisplayExclusionRegion.setEmpty();
 
             mShadowRadiusId = Resources.ID_NULL;
             mCornerRadius = 0;
@@ -830,6 +909,19 @@
         }
     }
 
+    private static class CaptionWindowlessWindowManager extends WindowlessWindowManager {
+        CaptionWindowlessWindowManager(
+                @NonNull Configuration configuration,
+                @NonNull SurfaceControl rootSurface) {
+            super(configuration, rootSurface, /* hostInputToken= */ null);
+        }
+
+        /** Set the view host's touchable region. */
+        void setTouchRegion(@NonNull SurfaceControlViewHost viewHost, @NonNull Region region) {
+            setTouchRegion(viewHost.getWindowToken().asBinder(), region);
+        }
+    }
+
     @VisibleForTesting
     public interface SurfaceControlViewHostFactory {
         default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) {
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt
index 4fe66f3..4cddf31 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt
@@ -23,8 +23,9 @@
 import android.tools.flicker.assertors.assertions.AppLayerIsInvisibleAtEnd
 import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways
 import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAtStart
-import android.tools.flicker.assertors.assertions.AppWindowBecomesVisible
 import android.tools.flicker.assertors.assertions.AppWindowAlignsWithOnlyOneDisplayCornerAtEnd
+import android.tools.flicker.assertors.assertions.AppWindowBecomesInvisible
+import android.tools.flicker.assertors.assertions.AppWindowBecomesVisible
 import android.tools.flicker.assertors.assertions.AppWindowCoversLeftHalfScreenAtEnd
 import android.tools.flicker.assertors.assertions.AppWindowCoversRightHalfScreenAtEnd
 import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd
@@ -44,6 +45,7 @@
 import android.tools.flicker.config.AssertionTemplates
 import android.tools.flicker.config.FlickerConfigEntry
 import android.tools.flicker.config.ScenarioId
+import android.tools.flicker.config.common.Components.LAUNCHER
 import android.tools.flicker.config.desktopmode.Components.DESKTOP_MODE_APP
 import android.tools.flicker.config.desktopmode.Components.DESKTOP_WALLPAPER
 import android.tools.flicker.config.desktopmode.Components.NON_RESIZABLE_APP
@@ -365,5 +367,57 @@
                             AppWindowAlignsWithOnlyOneDisplayCornerAtEnd(DESKTOP_MODE_APP)
                         ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
             )
+
+        val MINIMIZE_APP =
+            FlickerConfigEntry(
+                scenarioId = ScenarioId("MINIMIZE_APP"),
+                extractor =
+                ShellTransitionScenarioExtractor(
+                    transitionMatcher =
+                    object : ITransitionMatcher {
+                        override fun findAll(
+                            transitions: Collection<Transition>
+                        ): Collection<Transition> {
+                            return transitions
+                                .filter { it.type == TransitionType.MINIMIZE }
+                                .sortedByDescending { it.id }
+                                .drop(1)
+                        }
+                    }
+                ),
+                assertions =
+                AssertionTemplates.COMMON_ASSERTIONS +
+                    listOf(
+                        AppWindowOnTopAtStart(DESKTOP_MODE_APP),
+                        AppWindowBecomesInvisible(DESKTOP_MODE_APP),
+                    ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING })
+            )
+
+        val MINIMIZE_LAST_APP =
+            FlickerConfigEntry(
+                scenarioId = ScenarioId("MINIMIZE_LAST_APP"),
+                extractor =
+                ShellTransitionScenarioExtractor(
+                    transitionMatcher =
+                    object : ITransitionMatcher {
+                        override fun findAll(
+                            transitions: Collection<Transition>
+                        ): Collection<Transition> {
+                            val lastTransition =
+                                transitions
+                                .filter { it.type == TransitionType.MINIMIZE }
+                                .maxByOrNull { it.id }!!
+                            return listOf(lastTransition)
+                        }
+                    }
+                ),
+                assertions =
+                AssertionTemplates.COMMON_ASSERTIONS +
+                    listOf(
+                        AppWindowOnTopAtStart(DESKTOP_MODE_APP),
+                        AppWindowBecomesInvisible(DESKTOP_MODE_APP),
+                        AppWindowOnTopAtEnd(LAUNCHER),
+                    ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING })
+            )
     }
 }
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MinimizeAppsLandscape.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MinimizeAppsLandscape.kt
new file mode 100644
index 0000000..58582b0
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MinimizeAppsLandscape.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker
+
+import android.tools.Rotation.ROTATION_90
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MINIMIZE_APP
+import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MINIMIZE_LAST_APP
+import com.android.wm.shell.scenarios.MinimizeAppWindows
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Minimize app windows by pressing the minimize button.
+ *
+ * Assert that the app windows gets hidden.
+ */
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class MinimizeAppsLandscape : MinimizeAppWindows(rotation = ROTATION_90) {
+    @ExpectedScenarios(["MINIMIZE_APP", "MINIMIZE_LAST_APP"])
+    @Test
+    override fun minimizeAllAppWindows() = super.minimizeAllAppWindows()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig()
+                .use(FlickerServiceConfig.DEFAULT)
+                .use(MINIMIZE_APP)
+                .use(MINIMIZE_LAST_APP)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MinimizeAppsPortrait.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MinimizeAppsPortrait.kt
new file mode 100644
index 0000000..7970426
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MinimizeAppsPortrait.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker
+
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MINIMIZE_APP
+import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MINIMIZE_LAST_APP
+import com.android.wm.shell.scenarios.MinimizeAppWindows
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Minimize app windows by pressing the minimize button.
+ *
+ * Assert that the app windows gets hidden.
+ */
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class MinimizeAppsPortrait : MinimizeAppWindows() {
+    @ExpectedScenarios(["MINIMIZE_APP", "MINIMIZE_LAST_APP"])
+    @Test
+    override fun minimizeAllAppWindows() = super.minimizeAllAppWindows()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig()
+                .use(FlickerServiceConfig.DEFAULT)
+                .use(MINIMIZE_APP)
+                .use(MINIMIZE_LAST_APP)
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt
index 824c448..f442fdb 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt
@@ -18,6 +18,7 @@
 
 import android.tools.NavBar
 import android.tools.Rotation
+import com.android.internal.R
 import com.android.window.flags.Flags
 import com.android.wm.shell.Utils
 import org.junit.After
@@ -40,6 +41,9 @@
     @Before
     fun setup() {
         Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
+        // Skip the test when the drag-to-maximize is enabled on this device.
+        Assume.assumeFalse(Flags.enableDragToMaximize() &&
+            instrumentation.context.resources.getBoolean(R.bool.config_dragToMaximizeInDesktopMode))
         tapl.setEnableRotation(true)
         tapl.setExpectedRotation(rotation.value)
         testApp.enterDesktopWithDrag(wmHelper, device)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt
new file mode 100644
index 0000000..6df8d6f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt
@@ -0,0 +1,173 @@
+/*
+ * 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.wm.shell.desktopmode
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WindowingMode
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.view.SurfaceControl
+import android.view.WindowManager
+import android.view.WindowManager.TRANSIT_CLOSE
+import android.window.TransitionInfo
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestRunningTaskInfoBuilder
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.ShellExecutor
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+class DesktopBackNavigationTransitionHandlerTest : ShellTestCase() {
+
+    private val testExecutor = mock<ShellExecutor>()
+    private val closingTaskLeash = mock<SurfaceControl>()
+    private val displayController = mock<DisplayController>()
+
+    private lateinit var handler: DesktopBackNavigationTransitionHandler
+
+    @Before
+    fun setUp() {
+        handler =
+            DesktopBackNavigationTransitionHandler(
+                testExecutor,
+                testExecutor,
+                displayController
+            )
+        whenever(displayController.getDisplayContext(any())).thenReturn(mContext)
+    }
+
+    @Test
+    fun handleRequest_returnsNull() {
+        assertNull(handler.handleRequest(mock(), mock()))
+    }
+
+    @Test
+    fun startAnimation_openTransition_returnsFalse() {
+        val animates =
+            handler.startAnimation(
+                transition = mock(),
+                info =
+                createTransitionInfo(
+                    type = WindowManager.TRANSIT_OPEN,
+                    task = createTask(WINDOWING_MODE_FREEFORM)
+                ),
+                startTransaction = mock(),
+                finishTransaction = mock(),
+                finishCallback = {}
+            )
+
+        assertFalse("Should not animate open transition", animates)
+    }
+
+    @Test
+    fun startAnimation_toBackTransitionFullscreenTask_returnsFalse() {
+        val animates =
+            handler.startAnimation(
+                transition = mock(),
+                info = createTransitionInfo(task = createTask(WINDOWING_MODE_FULLSCREEN)),
+                startTransaction = mock(),
+                finishTransaction = mock(),
+                finishCallback = {}
+            )
+
+        assertFalse("Should not animate fullscreen task to back transition", animates)
+    }
+
+    @Test
+    fun startAnimation_toBackTransitionOpeningFreeformTask_returnsFalse() {
+        val animates =
+            handler.startAnimation(
+                transition = mock(),
+                info =
+                createTransitionInfo(
+                    changeMode = WindowManager.TRANSIT_OPEN,
+                    task = createTask(WINDOWING_MODE_FREEFORM)
+                ),
+                startTransaction = mock(),
+                finishTransaction = mock(),
+                finishCallback = {}
+            )
+
+        assertFalse("Should not animate opening freeform task to back transition", animates)
+    }
+
+    @Test
+    fun startAnimation_toBackTransitionToBackFreeformTask_returnsTrue() {
+        val animates =
+            handler.startAnimation(
+                transition = mock(),
+                info = createTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM)),
+                startTransaction = mock(),
+                finishTransaction = mock(),
+                finishCallback = {}
+            )
+
+        assertTrue("Should animate going to back freeform task close transition", animates)
+    }
+
+    @Test
+    fun startAnimation_closeTransitionClosingFreeformTask_returnsTrue() {
+        val animates =
+            handler.startAnimation(
+                transition = mock(),
+                info = createTransitionInfo(
+                    type = TRANSIT_CLOSE,
+                    changeMode = TRANSIT_CLOSE,
+                    task = createTask(WINDOWING_MODE_FREEFORM)
+                ),
+                startTransaction = mock(),
+                finishTransaction = mock(),
+                finishCallback = {}
+            )
+
+        assertTrue("Should animate going to back freeform task close transition", animates)
+    }
+    private fun createTransitionInfo(
+        type: Int = WindowManager.TRANSIT_TO_BACK,
+        changeMode: Int = WindowManager.TRANSIT_TO_BACK,
+        task: RunningTaskInfo
+    ): TransitionInfo =
+        TransitionInfo(type, 0 /* flags */).apply {
+            addChange(
+                TransitionInfo.Change(mock(), closingTaskLeash).apply {
+                    mode = changeMode
+                    parent = null
+                    taskInfo = task
+                }
+            )
+        }
+
+    private fun createTask(@WindowingMode windowingMode: Int): RunningTaskInfo =
+        TestRunningTaskInfoBuilder()
+            .setActivityType(ACTIVITY_TYPE_STANDARD)
+            .setWindowingMode(windowingMode)
+            .build()
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
index b06c2da..f21f264 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
@@ -32,6 +32,7 @@
 import android.view.SurfaceControl
 import android.view.WindowManager
 import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_TO_BACK
 import android.view.WindowManager.TransitionType
 import android.window.TransitionInfo
 import android.window.WindowContainerTransaction
@@ -77,16 +78,28 @@
 
     @JvmField @Rule val setFlagsRule = SetFlagsRule()
 
-    @Mock lateinit var transitions: Transitions
-    @Mock lateinit var desktopRepository: DesktopRepository
-    @Mock lateinit var freeformTaskTransitionHandler: FreeformTaskTransitionHandler
-    @Mock lateinit var closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler
-    @Mock lateinit var desktopImmersiveController: DesktopImmersiveController
-    @Mock lateinit var interactionJankMonitor: InteractionJankMonitor
-    @Mock lateinit var mockHandler: Handler
-    @Mock lateinit var closingTaskLeash: SurfaceControl
-    @Mock lateinit var shellInit: ShellInit
-    @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
+    @Mock
+    lateinit var transitions: Transitions
+    @Mock
+    lateinit var desktopRepository: DesktopRepository
+    @Mock
+    lateinit var freeformTaskTransitionHandler: FreeformTaskTransitionHandler
+    @Mock
+    lateinit var closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler
+    @Mock
+    lateinit var desktopBackNavigationTransitionHandler: DesktopBackNavigationTransitionHandler
+    @Mock
+    lateinit var desktopImmersiveController: DesktopImmersiveController
+    @Mock
+    lateinit var interactionJankMonitor: InteractionJankMonitor
+    @Mock
+    lateinit var mockHandler: Handler
+    @Mock
+    lateinit var closingTaskLeash: SurfaceControl
+    @Mock
+    lateinit var shellInit: ShellInit
+    @Mock
+    lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
 
     private lateinit var mixedHandler: DesktopMixedTransitionHandler
 
@@ -100,6 +113,7 @@
                 freeformTaskTransitionHandler,
                 closeDesktopTaskTransitionHandler,
                 desktopImmersiveController,
+                desktopBackNavigationTransitionHandler,
                 interactionJankMonitor,
                 mockHandler,
                 shellInit,
@@ -595,6 +609,87 @@
         assertThat(mixedHandler.pendingMixedTransitions).isEmpty()
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+    fun startAnimation_withMinimizingDesktopTask_callsBackNavigationHandler() {
+        val minimizingTask = createTask(WINDOWING_MODE_FREEFORM)
+        val transition = Binder()
+        whenever(desktopRepository.getExpandedTaskCount(any())).thenReturn(2)
+        whenever(
+            desktopBackNavigationTransitionHandler.startAnimation(any(), any(), any(), any(), any())
+        )
+            .thenReturn(true)
+        mixedHandler.addPendingMixedTransition(
+            PendingMixedTransition.Minimize(
+                transition = transition,
+                minimizingTask = minimizingTask.taskId,
+                isLastTask = false,
+            )
+        )
+
+        val minimizingTaskChange = createChange(minimizingTask)
+        val started = mixedHandler.startAnimation(
+            transition = transition,
+            info =
+                createTransitionInfo(
+                TRANSIT_TO_BACK,
+                listOf(minimizingTaskChange)
+            ),
+            startTransaction = mock(),
+            finishTransaction = mock(),
+            finishCallback = {}
+        )
+
+        assertTrue("Should delegate animation to back navigation transition handler", started)
+        verify(desktopBackNavigationTransitionHandler)
+            .startAnimation(
+                eq(transition),
+                argThat { info -> info.changes.contains(minimizingTaskChange) },
+                any(), any(), any())
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+    fun startAnimation_withMinimizingLastDesktopTask_dispatchesTransition() {
+        val minimizingTask = createTask(WINDOWING_MODE_FREEFORM)
+        val transition = Binder()
+        whenever(desktopRepository.getExpandedTaskCount(any())).thenReturn(2)
+        whenever(
+            desktopBackNavigationTransitionHandler.startAnimation(any(), any(), any(), any(), any())
+        )
+            .thenReturn(true)
+        mixedHandler.addPendingMixedTransition(
+            PendingMixedTransition.Minimize(
+                transition = transition,
+                minimizingTask = minimizingTask.taskId,
+                isLastTask = true,
+            )
+        )
+
+        val minimizingTaskChange = createChange(minimizingTask)
+        mixedHandler.startAnimation(
+            transition = transition,
+            info =
+            createTransitionInfo(
+                TRANSIT_TO_BACK,
+                listOf(minimizingTaskChange)
+            ),
+            startTransaction = mock(),
+            finishTransaction = mock(),
+            finishCallback = {}
+        )
+
+        verify(transitions)
+            .dispatchTransition(
+                eq(transition),
+                argThat { info -> info.changes.contains(minimizingTaskChange) },
+                any(),
+                any(),
+                any(),
+                eq(mixedHandler)
+            )
+    }
+
     private fun createTransitionInfo(
         type: Int = WindowManager.TRANSIT_CLOSE,
         changeMode: Int = WindowManager.TRANSIT_CLOSE,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 315a46f..ad266ea 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -3038,6 +3038,21 @@
     // Assert bounds set to stable bounds
     val wct = getLatestToggleResizeDesktopTaskWct()
     assertThat(findBoundsChange(wct, task)).isEqualTo(STABLE_BOUNDS)
+    // Assert event is properly logged
+    verify(desktopModeEventLogger, times(1)).logTaskResizingStarted(
+      ResizeTrigger.DRAG_TO_TOP_RESIZE_TRIGGER,
+      motionEvent,
+      task,
+      displayController
+    )
+    verify(desktopModeEventLogger, times(1)).logTaskResizingEnded(
+      ResizeTrigger.DRAG_TO_TOP_RESIZE_TRIGGER,
+      motionEvent,
+      task,
+      STABLE_BOUNDS.height(),
+      STABLE_BOUNDS.width(),
+      displayController
+    )
   }
 
   @Test
@@ -3082,6 +3097,13 @@
       eq(STABLE_BOUNDS),
       anyOrNull(),
     )
+    // Assert no event is logged
+    verify(desktopModeEventLogger, never()).logTaskResizingStarted(
+      any(), any(), any(), any(), any()
+    )
+    verify(desktopModeEventLogger, never()).logTaskResizingEnded(
+      any(), any(), any(), any(), any(), any(), any()
+    )
   }
 
   @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
index 737439c..7f1c1db 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
@@ -76,6 +76,7 @@
     private val context = mock<Context>()
     private val shellTaskOrganizer = mock<ShellTaskOrganizer>()
     private val taskRepository = mock<DesktopRepository>()
+    private val mixedHandler = mock<DesktopMixedTransitionHandler>()
 
     private lateinit var transitionObserver: DesktopTasksTransitionObserver
     private lateinit var shellInit: ShellInit
@@ -87,7 +88,7 @@
 
         transitionObserver =
             DesktopTasksTransitionObserver(
-                context, taskRepository, transitions, shellTaskOrganizer, shellInit
+                context, taskRepository, transitions, shellTaskOrganizer, mixedHandler, shellInit
             )
     }
 
@@ -106,6 +107,7 @@
         )
 
         verify(taskRepository).minimizeTask(task.displayId, task.taskId)
+        verify(mixedHandler).addPendingMixedTransition(any())
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java
index 72950a8..6d37ed7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java
@@ -28,8 +28,11 @@
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
+import android.app.AppCompatTaskInfo;
 import android.app.TaskInfo;
 import android.graphics.Rect;
 import android.testing.AndroidTestingRunner;
@@ -75,6 +78,7 @@
                 .setContainerLayer()
                 .setName("FakeLeash")
                 .build();
+        mTaskInfo.appCompatTaskInfo = mock(AppCompatTaskInfo.class);
     }
 
     @Test
@@ -93,7 +97,8 @@
         final Rect endValue1 = new Rect(100, 100, 200, 200);
         final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController
                 .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue1, null,
-                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0);
+                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0,
+                        false /* alwaysAnimateTaskBounds */);
 
         assertEquals("Expect ANIM_TYPE_BOUNDS animation",
                 animator.getAnimationType(), PipAnimationController.ANIM_TYPE_BOUNDS);
@@ -107,14 +112,16 @@
         final Rect endValue2 = new Rect(200, 200, 300, 300);
         final PipAnimationController.PipTransitionAnimator oldAnimator = mPipAnimationController
                 .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue1, null,
-                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0);
+                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0,
+                        false /* alwaysAnimateTaskBounds */);
         oldAnimator.setSurfaceControlTransactionFactory(
                 MockSurfaceControlHelper::createMockSurfaceControlTransaction);
         oldAnimator.start();
 
         final PipAnimationController.PipTransitionAnimator newAnimator = mPipAnimationController
                 .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue2, null,
-                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0);
+                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0,
+                        false /* alwaysAnimateTaskBounds */);
 
         assertEquals("getAnimator with same type returns same animator",
                 oldAnimator, newAnimator);
@@ -145,7 +152,8 @@
         // Fullscreen to PiP.
         PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController
                 .getAnimator(mTaskInfo, mLeash, null, startBounds, endBounds, null,
-                        TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_90);
+                        TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_90,
+                        false /* alwaysAnimateTaskBounds */);
         // Apply fraction 1 to compute the end value.
         animator.applySurfaceControlTransaction(mLeash, tx, 1);
         final Rect rotatedEndBounds = new Rect(endBounds);
@@ -157,7 +165,8 @@
         startBounds.set(0, 0, 1000, 500);
         endBounds.set(200, 100, 400, 500);
         animator = mPipAnimationController.getAnimator(mTaskInfo, mLeash, startBounds, startBounds,
-                endBounds, null, TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_270);
+                endBounds, null, TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_270,
+                false /* alwaysAnimateTaskBounds */);
         animator.applySurfaceControlTransaction(mLeash, tx, 1);
         rotatedEndBounds.set(endBounds);
         rotateBounds(rotatedEndBounds, startBounds, ROTATION_270);
@@ -166,6 +175,37 @@
     }
 
     @Test
+    public void pipTransitionAnimator_rotatedEndValue_overrideMainWindowFrame() {
+        final SurfaceControl.Transaction tx = createMockSurfaceControlTransaction();
+        final Rect startBounds = new Rect(200, 700, 400, 800);
+        final Rect endBounds = new Rect(0, 0, 500, 1000);
+        mTaskInfo.topActivityMainWindowFrame = new Rect(0, 250, 1000, 500);
+
+        // Fullscreen task to PiP.
+        PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController
+                .getAnimator(mTaskInfo, mLeash, null, startBounds, endBounds, null,
+                        TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_90,
+                        false /* alwaysAnimateTaskBounds */);
+        // Apply fraction 1 to compute the end value.
+        animator.applySurfaceControlTransaction(mLeash, tx, 1);
+
+        assertEquals("Expect use main window frame", mTaskInfo.topActivityMainWindowFrame,
+                animator.mCurrentValue);
+
+        // PiP to fullscreen.
+        mTaskInfo.topActivityMainWindowFrame = new Rect(0, 250, 1000, 500);
+        startBounds.set(0, 0, 1000, 500);
+        endBounds.set(200, 100, 400, 500);
+        animator = mPipAnimationController.getAnimator(mTaskInfo, mLeash, startBounds, startBounds,
+                endBounds, null, TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_270,
+                false /* alwaysAnimateTaskBounds */);
+        animator.applySurfaceControlTransaction(mLeash, tx, 1);
+
+        assertEquals("Expect use main window frame", mTaskInfo.topActivityMainWindowFrame,
+                animator.mCurrentValue);
+    }
+
+    @Test
     @SuppressWarnings("unchecked")
     public void pipTransitionAnimator_updateEndValue() {
         final Rect baseValue = new Rect(0, 0, 100, 100);
@@ -174,7 +214,8 @@
         final Rect endValue2 = new Rect(200, 200, 300, 300);
         final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController
                 .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue1, null,
-                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0);
+                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0,
+                        false /* alwaysAnimateTaskBounds */);
 
         animator.updateEndValue(endValue2);
 
@@ -188,7 +229,8 @@
         final Rect endValue = new Rect(100, 100, 200, 200);
         final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController
                 .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue, null,
-                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0);
+                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0,
+                        false /* alwaysAnimateTaskBounds */);
         animator.setSurfaceControlTransactionFactory(
                 MockSurfaceControlHelper::createMockSurfaceControlTransaction);
 
@@ -207,4 +249,126 @@
         verify(mPipAnimationCallback).onPipAnimationEnd(eq(mTaskInfo),
                 any(SurfaceControl.Transaction.class), eq(animator));
     }
+
+    @Test
+    public void pipTransitionAnimator_overrideMainWindowFrame() {
+        final Rect baseValue = new Rect(0, 0, 100, 100);
+        final Rect startValue = new Rect(0, 0, 100, 100);
+        final Rect endValue = new Rect(100, 100, 200, 200);
+        mTaskInfo.topActivityMainWindowFrame = new Rect(0, 50, 100, 100);
+        PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController
+                .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue, null,
+                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0,
+                        false /* alwaysAnimateTaskBounds */);
+
+        assertEquals("Expect base value is overridden for in-PIP transition",
+                mTaskInfo.topActivityMainWindowFrame, animator.getBaseValue());
+        assertEquals("Expect start value is overridden for in-PIP transition",
+                mTaskInfo.topActivityMainWindowFrame, animator.getStartValue());
+        assertEquals("Expect end value is not overridden for in-PIP transition",
+                endValue, animator.getEndValue());
+
+        animator = mPipAnimationController.getAnimator(mTaskInfo, mLeash, baseValue, startValue,
+                endValue, null, TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_0,
+                        false /* alwaysAnimateTaskBounds */);
+
+        assertEquals("Expect base value is not overridden for leave-PIP transition",
+                baseValue, animator.getBaseValue());
+        assertEquals("Expect start value is not overridden for leave-PIP transition",
+                startValue, animator.getStartValue());
+        assertEquals("Expect end value is overridden for leave-PIP transition",
+                mTaskInfo.topActivityMainWindowFrame, animator.getEndValue());
+    }
+
+    @Test
+    public void pipTransitionAnimator_animateTaskBounds() {
+        final Rect baseValue = new Rect(0, 0, 100, 100);
+        final Rect startValue = new Rect(0, 0, 100, 100);
+        final Rect endValue = new Rect(100, 100, 200, 200);
+        mTaskInfo.topActivityMainWindowFrame = new Rect(0, 50, 100, 100);
+        PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController
+                .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue, null,
+                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0,
+                        true /* alwaysAnimateTaskBounds */);
+
+        assertEquals("Expect base value is not overridden for in-PIP transition",
+                baseValue, animator.getBaseValue());
+        assertEquals("Expect start value is not overridden for in-PIP transition",
+                startValue, animator.getStartValue());
+        assertEquals("Expect end value is not overridden for in-PIP transition",
+                endValue, animator.getEndValue());
+
+        animator = mPipAnimationController.getAnimator(mTaskInfo, mLeash, baseValue, startValue,
+                endValue, null, TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_0,
+                true /* alwaysAnimateTaskBounds */);
+
+        assertEquals("Expect base value is not overridden for leave-PIP transition",
+                baseValue, animator.getBaseValue());
+        assertEquals("Expect start value is not overridden for leave-PIP transition",
+                startValue, animator.getStartValue());
+        assertEquals("Expect end value is not overridden for leave-PIP transition",
+                endValue, animator.getEndValue());
+    }
+
+    @Test
+    public void pipTransitionAnimator_letterboxed_animateTaskBounds() {
+        final Rect baseValue = new Rect(0, 0, 100, 100);
+        final Rect startValue = new Rect(0, 0, 100, 100);
+        final Rect endValue = new Rect(100, 100, 200, 200);
+        mTaskInfo.topActivityMainWindowFrame = new Rect(0, 50, 100, 100);
+        doReturn(true).when(mTaskInfo.appCompatTaskInfo).isTopActivityLetterboxed();
+        PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController
+                .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue, null,
+                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0,
+                        false /* alwaysAnimateTaskBounds */);
+
+        assertEquals("Expect base value is not overridden for in-PIP transition",
+                baseValue, animator.getBaseValue());
+        assertEquals("Expect start value is not overridden for in-PIP transition",
+                startValue, animator.getStartValue());
+        assertEquals("Expect end value is not overridden for in-PIP transition",
+                endValue, animator.getEndValue());
+
+        animator = mPipAnimationController.getAnimator(mTaskInfo, mLeash, baseValue, startValue,
+                endValue, null, TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_0,
+                false /* alwaysAnimateTaskBounds */);
+
+        assertEquals("Expect base value is not overridden for leave-PIP transition",
+                baseValue, animator.getBaseValue());
+        assertEquals("Expect start value is not overridden for leave-PIP transition",
+                startValue, animator.getStartValue());
+        assertEquals("Expect end value is not overridden for leave-PIP transition",
+                endValue, animator.getEndValue());
+    }
+
+    @Test
+    public void pipTransitionAnimator_sizeCompat_animateTaskBounds() {
+        final Rect baseValue = new Rect(0, 0, 100, 100);
+        final Rect startValue = new Rect(0, 0, 100, 100);
+        final Rect endValue = new Rect(100, 100, 200, 200);
+        mTaskInfo.topActivityMainWindowFrame = new Rect(0, 50, 100, 100);
+        doReturn(true).when(mTaskInfo.appCompatTaskInfo).isTopActivityInSizeCompat();
+        PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController
+                .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue, null,
+                        TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0,
+                        false /* alwaysAnimateTaskBounds */);
+
+        assertEquals("Expect base value is not overridden for in-PIP transition",
+                baseValue, animator.getBaseValue());
+        assertEquals("Expect start value is not overridden for in-PIP transition",
+                startValue, animator.getStartValue());
+        assertEquals("Expect end value is not overridden for in-PIP transition",
+                endValue, animator.getEndValue());
+
+        animator = mPipAnimationController.getAnimator(mTaskInfo, mLeash, baseValue, startValue,
+                endValue, null, TRANSITION_DIRECTION_LEAVE_PIP, 0, ROTATION_0,
+                false /* alwaysAnimateTaskBounds */);
+
+        assertEquals("Expect base value is not overridden for leave-PIP transition",
+                baseValue, animator.getBaseValue());
+        assertEquals("Expect start value is not overridden for leave-PIP transition",
+                startValue, animator.getStartValue());
+        assertEquals("Expect end value is not overridden for leave-PIP transition",
+                endValue, animator.getEndValue());
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt
index 5ebf517..59141ca 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt
@@ -19,6 +19,7 @@
 import android.app.ActivityManager
 import android.app.WindowConfiguration
 import android.content.ComponentName
+import android.graphics.Region
 import android.testing.AndroidTestingRunner
 import android.view.Display
 import android.view.InsetsState
@@ -33,6 +34,9 @@
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
 class CaptionWindowDecorationTests : ShellTestCase() {
+
+    private val exclusionRegion = Region.obtain()
+
     @Test
     fun updateRelayoutParams_freeformAndTransparentAppearance_allowsInputFallthrough() {
         val taskInfo = createTaskInfo()
@@ -50,7 +54,8 @@
             true /* isStatusBarVisible */,
             false /* isKeyguardVisibleAndOccluded */,
             InsetsState(),
-            true /* hasGlobalFocus */
+            true /* hasGlobalFocus */,
+            exclusionRegion
         )
 
         Truth.assertThat(relayoutParams.hasInputFeatureSpy()).isTrue()
@@ -72,7 +77,8 @@
             true /* isStatusBarVisible */,
             false /* isKeyguardVisibleAndOccluded */,
             InsetsState(),
-            true /* hasGlobalFocus */
+            true /* hasGlobalFocus */,
+            exclusionRegion
         )
 
         Truth.assertThat(relayoutParams.hasInputFeatureSpy()).isFalse()
@@ -90,7 +96,8 @@
             true /* isStatusBarVisible */,
             false /* isKeyguardVisibleAndOccluded */,
             InsetsState(),
-            true /* hasGlobalFocus */
+            true /* hasGlobalFocus */,
+            exclusionRegion
         )
         Truth.assertThat(relayoutParams.mOccludingCaptionElements.size).isEqualTo(2)
         Truth.assertThat(relayoutParams.mOccludingCaptionElements[0].mAlignment).isEqualTo(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index 956100d..be664f8 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -30,6 +30,7 @@
 import android.content.Intent.ACTION_MAIN
 import android.content.pm.ActivityInfo
 import android.graphics.Rect
+import android.graphics.Region
 import android.hardware.display.DisplayManager
 import android.hardware.display.VirtualDisplay
 import android.hardware.input.InputManager
@@ -48,6 +49,7 @@
 import android.util.SparseArray
 import android.view.Choreographer
 import android.view.Display.DEFAULT_DISPLAY
+import android.view.ISystemGestureExclusionListener
 import android.view.IWindowManager
 import android.view.InputChannel
 import android.view.InputMonitor
@@ -84,7 +86,6 @@
 import com.android.wm.shell.common.DisplayInsetsController
 import com.android.wm.shell.common.DisplayLayout
 import com.android.wm.shell.common.MultiInstanceHelper
-import com.android.wm.shell.common.ShellExecutor
 import com.android.wm.shell.common.SyncTransactionQueue
 import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger
@@ -131,6 +132,7 @@
 import org.mockito.kotlin.KArgumentCaptor
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
 import org.mockito.kotlin.argThat
 import org.mockito.kotlin.argumentCaptor
 import org.mockito.kotlin.doNothing
@@ -175,7 +177,7 @@
     @Mock private lateinit var mockInputMonitorFactory:
             DesktopModeWindowDecorViewModel.InputMonitorFactory
     @Mock private lateinit var mockShellController: ShellController
-    @Mock private lateinit var mockShellExecutor: ShellExecutor
+    private val testShellExecutor = TestShellExecutor()
     @Mock private lateinit var mockAppHeaderViewHolderFactory: AppHeaderViewHolder.Factory
     @Mock private lateinit var mockRootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
     @Mock private lateinit var mockShellCommandHandler: ShellCommandHandler
@@ -230,13 +232,13 @@
 
         spyContext = spy(mContext)
         doNothing().`when`(spyContext).startActivity(any())
-        shellInit = ShellInit(mockShellExecutor)
+        shellInit = ShellInit(testShellExecutor)
         windowDecorByTaskIdSpy.clear()
         spyContext.addMockSystemService(InputManager::class.java, mockInputManager)
         desktopModeEventLogger = mock<DesktopModeEventLogger>()
         desktopModeWindowDecorViewModel = DesktopModeWindowDecorViewModel(
                 spyContext,
-                mockShellExecutor,
+                testShellExecutor,
                 mockMainHandler,
                 mockMainChoreographer,
                 bgExecutor,
@@ -1321,11 +1323,11 @@
 
         decoration.mHasGlobalFocus = true
         desktopModeWindowDecorViewModel.onTaskInfoChanged(task)
-        verify(decoration).relayout(task, true)
+        verify(decoration).relayout(eq(task), eq(true), anyOrNull())
 
         decoration.mHasGlobalFocus = false
         desktopModeWindowDecorViewModel.onTaskInfoChanged(task)
-        verify(decoration).relayout(task, false)
+        verify(decoration).relayout(eq(task), eq(false), anyOrNull())
     }
 
     @Test
@@ -1342,17 +1344,66 @@
 
         task.isFocused = true
         desktopModeWindowDecorViewModel.onTaskInfoChanged(task)
-        verify(decoration).relayout(task, true)
+        verify(decoration).relayout(eq(task), eq(true), anyOrNull())
 
         task.isFocused = false
         desktopModeWindowDecorViewModel.onTaskInfoChanged(task)
-        verify(decoration).relayout(task, false)
+        verify(decoration).relayout(eq(task), eq(false), anyOrNull())
+    }
+
+    @Test
+    fun testGestureExclusionChanged_updatesDecorations() {
+        val captor = argumentCaptor<ISystemGestureExclusionListener>()
+        verify(mockWindowManager)
+            .registerSystemGestureExclusionListener(captor.capture(), eq(DEFAULT_DISPLAY))
+        val task = createOpenTaskDecoration(
+            windowingMode = WINDOWING_MODE_FREEFORM,
+            displayId = DEFAULT_DISPLAY
+        )
+        val task2 = createOpenTaskDecoration(
+            windowingMode = WINDOWING_MODE_FREEFORM,
+            displayId = DEFAULT_DISPLAY
+        )
+        val newRegion = Region.obtain().apply {
+            set(Rect(0, 0, 1600, 80))
+        }
+
+        captor.firstValue.onSystemGestureExclusionChanged(DEFAULT_DISPLAY, newRegion, newRegion)
+        testShellExecutor.flushAll()
+
+        verify(task).onExclusionRegionChanged(newRegion)
+        verify(task2).onExclusionRegionChanged(newRegion)
+    }
+
+    @Test
+    fun testGestureExclusionChanged_otherDisplay_skipsDecorationUpdate() {
+        val captor = argumentCaptor<ISystemGestureExclusionListener>()
+        verify(mockWindowManager)
+            .registerSystemGestureExclusionListener(captor.capture(), eq(DEFAULT_DISPLAY))
+        val task = createOpenTaskDecoration(
+            windowingMode = WINDOWING_MODE_FREEFORM,
+            displayId = DEFAULT_DISPLAY
+        )
+        val task2 = createOpenTaskDecoration(
+            windowingMode = WINDOWING_MODE_FREEFORM,
+            displayId = 2
+        )
+        val newRegion = Region.obtain().apply {
+            set(Rect(0, 0, 1600, 80))
+        }
+
+        captor.firstValue.onSystemGestureExclusionChanged(DEFAULT_DISPLAY, newRegion, newRegion)
+        testShellExecutor.flushAll()
+
+        verify(task).onExclusionRegionChanged(newRegion)
+        verify(task2, never()).onExclusionRegionChanged(newRegion)
     }
 
     private fun createOpenTaskDecoration(
         @WindowingMode windowingMode: Int,
         taskSurface: SurfaceControl = SurfaceControl(),
         requestingImmersive: Boolean = false,
+        displayId: Int = DEFAULT_DISPLAY,
         onMaxOrRestoreListenerCaptor: ArgumentCaptor<Function0<Unit>> =
             forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>>,
         onImmersiveOrRestoreListenerCaptor: KArgumentCaptor<() -> Unit> =
@@ -1376,6 +1427,7 @@
     ): DesktopModeWindowDecoration {
         val decor = setUpMockDecorationForTask(createTask(
             windowingMode = windowingMode,
+            displayId = displayId,
             requestingImmersive = requestingImmersive
         ))
         onTaskOpening(decor.mTaskInfo, taskSurface)
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 41f57ae..1d2d0f0 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
@@ -64,6 +64,7 @@
 import android.content.res.TypedArray;
 import android.graphics.PointF;
 import android.graphics.Rect;
+import android.graphics.Region;
 import android.net.Uri;
 import android.os.Handler;
 import android.os.SystemProperties;
@@ -224,6 +225,7 @@
     private TestableContext mTestableContext;
     private final ShellExecutor mBgExecutor = new TestShellExecutor();
     private final AssistContent mAssistContent = new AssistContent();
+    private final Region mExclusionRegion = Region.obtain();
 
     /** Set up run before test class. */
     @BeforeClass
@@ -262,8 +264,8 @@
         doReturn(defaultDisplay).when(mMockDisplayController).getDisplay(Display.DEFAULT_DISPLAY);
         doReturn(mInsetsState).when(mMockDisplayController).getInsetsState(anyInt());
         when(mMockHandleMenuFactory.create(any(), any(), anyInt(), any(), any(), any(),
-                anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), any(), anyInt(), anyInt(),
-                anyInt(), anyInt()))
+                anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), any(),
+                anyInt(), anyInt(), anyInt(), anyInt()))
                 .thenReturn(mMockHandleMenu);
         when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any())).thenReturn(false);
         when(mMockAppHeaderViewHolderFactory.create(any(), any(), any(), any(), any(), any(), any(),
@@ -283,7 +285,7 @@
         final DesktopModeWindowDecoration spyWindowDecor =
                 spy(createWindowDecoration(taskInfo));
 
-        spyWindowDecor.relayout(taskInfo, false /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, false /* hasGlobalFocus */, mExclusionRegion);
 
         // Menus should close if open before the task being invisible causes relayout to return.
         verify(spyWindowDecor).closeHandleMenu();
@@ -303,7 +305,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.mShadowRadiusId).isNotEqualTo(Resources.ID_NULL);
     }
@@ -324,7 +327,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.mCornerRadius).isGreaterThan(0);
     }
@@ -350,7 +354,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.mWindowDecorConfig.densityDpi).isEqualTo(customTaskDensity);
     }
@@ -377,12 +382,14 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.mWindowDecorConfig.densityDpi).isEqualTo(systemDensity);
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS)
     public void updateRelayoutParams_freeformAndTransparentAppearance_allowsInputFallthrough() {
         final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
         taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
@@ -400,12 +407,39 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.hasInputFeatureSpy()).isTrue();
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS)
+    public void updateRelayoutParams_freeformAndTransparentAppearance_limitedTouchRegion() {
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+        taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+        taskInfo.taskDescription.setTopOpaqueSystemBarsAppearance(
+                APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND);
+        final RelayoutParams relayoutParams = new RelayoutParams();
+
+        DesktopModeWindowDecoration.updateRelayoutParams(
+                relayoutParams,
+                mTestableContext,
+                taskInfo,
+                /* applyStartTransactionOnDraw= */ true,
+                /* shouldSetTaskPositionAndCrop */ false,
+                /* isStatusBarVisible */ true,
+                /* isKeyguardVisibleAndOccluded */ false,
+                /* inFullImmersiveMode */ false,
+                new InsetsState(),
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
+
+        assertThat(relayoutParams.mLimitTouchRegionToSystemAreas).isTrue();
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS)
     public void updateRelayoutParams_freeformButOpaqueAppearance_disallowsInputFallthrough() {
         final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
         taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
@@ -422,12 +456,38 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.hasInputFeatureSpy()).isFalse();
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS)
+    public void updateRelayoutParams_freeformButOpaqueAppearance_unlimitedTouchRegion() {
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+        taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+        taskInfo.taskDescription.setTopOpaqueSystemBarsAppearance(0);
+        final RelayoutParams relayoutParams = new RelayoutParams();
+
+        DesktopModeWindowDecoration.updateRelayoutParams(
+                relayoutParams,
+                mTestableContext,
+                taskInfo,
+                /* applyStartTransactionOnDraw= */ true,
+                /* shouldSetTaskPositionAndCrop */ false,
+                /* isStatusBarVisible */ true,
+                /* isKeyguardVisibleAndOccluded */ false,
+                /* inFullImmersiveMode */ false,
+                new InsetsState(),
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
+
+        assertThat(relayoutParams.mLimitTouchRegionToSystemAreas).isFalse();
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS)
     public void updateRelayoutParams_fullscreen_disallowsInputFallthrough() {
         final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
         taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
@@ -443,12 +503,36 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.hasInputFeatureSpy()).isFalse();
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_ACCESSIBLE_CUSTOM_HEADERS)
+    public void updateRelayoutParams_fullscreen_unlimitedTouchRegion() {
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+        taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+        final RelayoutParams relayoutParams = new RelayoutParams();
+
+        DesktopModeWindowDecoration.updateRelayoutParams(
+                relayoutParams,
+                mTestableContext,
+                taskInfo,
+                /* applyStartTransactionOnDraw= */ true,
+                /* shouldSetTaskPositionAndCrop */ false,
+                /* isStatusBarVisible */ true,
+                /* isKeyguardVisibleAndOccluded */ false,
+                /* inFullImmersiveMode */ false,
+                new InsetsState(),
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
+
+        assertThat(relayoutParams.mLimitTouchRegionToSystemAreas).isFalse();
+    }
+
+    @Test
     public void updateRelayoutParams_freeform_inputChannelNeeded() {
         final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
         taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
@@ -464,7 +548,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(hasNoInputChannelFeature(relayoutParams)).isFalse();
     }
@@ -486,7 +571,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(hasNoInputChannelFeature(relayoutParams)).isTrue();
     }
@@ -508,7 +594,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(hasNoInputChannelFeature(relayoutParams)).isTrue();
     }
@@ -531,7 +618,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat((relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING) != 0).isTrue();
     }
@@ -555,7 +643,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat((relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING) == 0).isTrue();
     }
@@ -577,7 +666,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(
                 (relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR) != 0)
@@ -601,7 +691,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(
                 (relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR) == 0)
@@ -631,7 +722,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ true,
                 insetsState,
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         // Takes status bar inset as padding, ignores caption bar inset.
         assertThat(relayoutParams.mCaptionTopPadding).isEqualTo(50);
@@ -654,7 +746,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ true,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.mIsInsetSource).isFalse();
     }
@@ -676,7 +769,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         // Header is always shown because it's assumed the status bar is always visible.
         assertThat(relayoutParams.mIsCaptionVisible).isTrue();
@@ -698,7 +792,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.mIsCaptionVisible).isTrue();
     }
@@ -719,7 +814,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.mIsCaptionVisible).isFalse();
     }
@@ -740,7 +836,8 @@
                 /* isKeyguardVisibleAndOccluded */ true,
                 /* inFullImmersiveMode */ false,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.mIsCaptionVisible).isFalse();
     }
@@ -762,7 +859,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ true,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.mIsCaptionVisible).isTrue();
 
@@ -776,7 +874,8 @@
                 /* isKeyguardVisibleAndOccluded */ false,
                 /* inFullImmersiveMode */ true,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.mIsCaptionVisible).isFalse();
     }
@@ -798,7 +897,8 @@
                 /* isKeyguardVisibleAndOccluded */ true,
                 /* inFullImmersiveMode */ true,
                 new InsetsState(),
-                /* hasGlobalFocus= */ true);
+                /* hasGlobalFocus= */ true,
+                mExclusionRegion);
 
         assertThat(relayoutParams.mIsCaptionVisible).isFalse();
     }
@@ -809,7 +909,7 @@
         final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
         taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
 
-        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
 
         verify(mMockTransaction).apply();
         verify(mMockRootSurfaceControl, never()).applyTransactionOnDraw(any());
@@ -824,7 +924,7 @@
         // Make non-resizable to avoid dealing with input-permissions (MONITOR_INPUT)
         taskInfo.isResizeable = false;
 
-        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
 
         verify(mMockTransaction, never()).apply();
         verify(mMockRootSurfaceControl).applyTransactionOnDraw(mMockTransaction);
@@ -836,7 +936,7 @@
         final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
         taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
 
-        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
 
         verify(mMockSurfaceControlViewHostFactory, never()).create(any(), any(), any());
     }
@@ -848,7 +948,7 @@
         taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
 
         ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class);
-        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
 
         // Once for view host, the other for the AppHandle input layer.
         verify(mMockHandler, times(2)).post(runnableArgument.capture());
@@ -865,7 +965,7 @@
         // Make non-resizable to avoid dealing with input-permissions (MONITOR_INPUT)
         taskInfo.isResizeable = false;
 
-        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
 
         verify(mMockSurfaceControlViewHostFactory).create(any(), any(), any());
         verify(mMockHandler, never()).post(any());
@@ -877,11 +977,11 @@
         final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
         taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
         ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class);
-        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
         // Once for view host, the other for the AppHandle input layer.
         verify(mMockHandler, times(2)).post(runnableArgument.capture());
 
-        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
 
         verify(mMockHandler).removeCallbacks(runnableArgument.getValue());
     }
@@ -892,7 +992,7 @@
         final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
         taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
         ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class);
-        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
         // Once for view host, the other for the AppHandle input layer.
         verify(mMockHandler, times(2)).post(runnableArgument.capture());
 
@@ -1132,7 +1232,7 @@
         runnableArgument.getValue().run();
 
         // Relayout decor with same captured link
-        decor.relayout(taskInfo, true /* hasGlobalFocus */);
+        decor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
 
         // Verify handle menu's browser link not set to captured link since link is expired
         createHandleMenu(decor);
@@ -1313,7 +1413,7 @@
         final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
         taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
 
-        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
 
         verify(mMockCaptionHandleRepository, never()).notifyCaptionChanged(any());
     }
@@ -1330,7 +1430,7 @@
         ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
                 CaptionState.class);
 
-        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
 
         verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged(
                 captionStateArgumentCaptor.capture());
@@ -1357,7 +1457,7 @@
         ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
                 CaptionState.class);
 
-        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
         verify(mMockAppHeaderViewHolder, atLeastOnce()).runOnAppChipGlobalLayout(
                 runnableArgumentCaptor.capture());
         runnableArgumentCaptor.getValue().invoke();
@@ -1380,7 +1480,7 @@
         ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
                 CaptionState.class);
 
-        spyWindowDecor.relayout(taskInfo, false /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, false /* hasGlobalFocus */, mExclusionRegion);
 
         verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged(
                 captionStateArgumentCaptor.capture());
@@ -1400,7 +1500,7 @@
         ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
                 CaptionState.class);
 
-        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
         createHandleMenu(spyWindowDecor);
 
         verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged(
@@ -1425,7 +1525,7 @@
         ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
                 CaptionState.class);
 
-        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+        spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
         createHandleMenu(spyWindowDecor);
         spyWindowDecor.closeHandleMenu();
 
@@ -1440,9 +1540,30 @@
 
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB)
+    public void browserApp_webUriUsedForBrowserApp() {
+        // Make {@link AppToWebUtils#isBrowserApp} return true
+        ResolveInfo resolveInfo = new ResolveInfo();
+        resolveInfo.handleAllWebDataURI = true;
+        resolveInfo.activityInfo = createActivityInfo();
+        when(mMockPackageManager.queryIntentActivitiesAsUser(any(), anyInt(), anyInt()))
+                .thenReturn(List.of(resolveInfo));
+
+        final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
+        final DesktopModeWindowDecoration decor = createWindowDecoration(
+                taskInfo, TEST_URI1 /* captured link */, TEST_URI2 /* web uri */,
+                TEST_URI3 /* generic link */);
+
+        // Verify web uri used for browser applications
+        createHandleMenu(decor);
+        verifyHandleMenuCreated(TEST_URI2);
+    }
+
+
     private void verifyHandleMenuCreated(@Nullable Uri uri) {
         verify(mMockHandleMenuFactory).create(any(), any(), anyInt(), any(), any(),
-                any(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(),
+                any(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(),
                 argThat(intent -> (uri == null && intent == null) || intent.getData().equals(uri)),
                 anyInt(), anyInt(), anyInt(), anyInt());
     }
@@ -1522,7 +1643,7 @@
         windowDecor.setOpenInBrowserClickListener(mMockOpenInBrowserClickListener);
         windowDecor.mDecorWindowContext = mContext;
         if (relayout) {
-            windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
+            windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, mExclusionRegion);
         }
         return windowDecor;
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
index ade17c6..7ec2cbf 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
@@ -242,7 +242,7 @@
 
     private fun createAndShowHandleMenu(
         splitPosition: Int? = null,
-        forceShowSystemBars: Boolean = false,
+        forceShowSystemBars: Boolean = false
     ): HandleMenu {
         val layoutId = if (mockDesktopWindowDecoration.mTaskInfo.isFreeform) {
             R.layout.desktop_mode_app_header
@@ -266,8 +266,9 @@
             WindowManagerWrapper(mockWindowManager),
             layoutId, appIcon, appName, splitScreenController, shouldShowWindowingPill = true,
             shouldShowNewWindowButton = true, shouldShowManageWindowsButton = false,
-            shouldShowChangeAspectRatioButton = false,
-            null /* openInBrowserLink */, captionWidth = HANDLE_WIDTH, captionHeight = 50,
+            shouldShowChangeAspectRatioButton = false, isBrowserApp = false,
+            null /* openInAppOrBrowserIntent */, captionWidth = HANDLE_WIDTH,
+            captionHeight = 50,
             captionX = captionX,
             captionY = 0,
         )
@@ -278,7 +279,7 @@
             onNewWindowClickListener = mock(),
             onManageWindowsClickListener = mock(),
             onChangeAspectRatioClickListener = mock(),
-            openInBrowserClickListener = mock(),
+            openInAppOrBrowserClickListener = mock(),
             onOpenByDefaultClickListener = mock(),
             onCloseMenuClickListener = mock(),
             onOutsideTouchListener = mock(),
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index 8e0434c..534803d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -60,6 +60,7 @@
 import android.graphics.Color;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.graphics.Region;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.testing.AndroidTestingRunner;
 import android.util.DisplayMetrics;
@@ -508,7 +509,7 @@
         final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
 
         windowDecor.relayout(taskInfo, true /* applyStartTransactionOnDraw */,
-                true /* hasGlobalFocus */);
+                true /* hasGlobalFocus */, Region.obtain());
 
         verify(mMockRootSurfaceControl).applyTransactionOnDraw(mMockSurfaceControlStartT);
     }
@@ -525,7 +526,7 @@
         mRelayoutParams.mCaptionTopPadding = 50;
 
         windowDecor.relayout(taskInfo, false /* applyStartTransactionOnDraw */,
-                true /* hasGlobalFocus */);
+                true /* hasGlobalFocus */, Region.obtain());
 
         assertEquals(50, mRelayoutResult.mCaptionTopPadding);
     }
@@ -944,7 +945,7 @@
 
         decor.onInsetsStateChanged(createInsetsState(statusBars(), false /* visible */));
 
-        verify(decor, times(2)).relayout(task, true /* hasGlobalFocus */);
+        verify(decor, times(2)).relayout(any(), any(), any(), any(), any(), any());
     }
 
     @Test
@@ -958,7 +959,7 @@
 
         decor.onInsetsStateChanged(createInsetsState(statusBars(), true /* visible */));
 
-        verify(decor, times(1)).relayout(task, true /* hasGlobalFocus */);
+        verify(decor, times(1)).relayout(any(), any(), any(), any(), any(), any());
     }
 
     @Test
@@ -973,7 +974,7 @@
         decor.onKeyguardStateChanged(true /* visible */, true /* occluding */);
 
         assertTrue(decor.mIsKeyguardVisibleAndOccluded);
-        verify(decor, times(2)).relayout(task, true /* hasGlobalFocus */);
+        verify(decor, times(2)).relayout(any(), any(), any(), any(), any(), any());
     }
 
     @Test
@@ -987,7 +988,7 @@
 
         decor.onKeyguardStateChanged(false /* visible */, true /* occluding */);
 
-        verify(decor, times(1)).relayout(task, true /* hasGlobalFocus */);
+        verify(decor, times(1)).relayout(any(), any(), any(), any(), any(), any());
     }
 
     private ActivityManager.RunningTaskInfo createTaskInfo() {
@@ -1061,9 +1062,16 @@
                     surfaceControlViewHostFactory, desktopModeEventLogger);
         }
 
-        @Override
         void relayout(ActivityManager.RunningTaskInfo taskInfo, boolean hasGlobalFocus) {
-            relayout(taskInfo, false /* applyStartTransactionOnDraw */, hasGlobalFocus);
+            relayout(taskInfo, false /* applyStartTransactionOnDraw */, hasGlobalFocus,
+                    Region.obtain());
+        }
+
+        @Override
+        void relayout(ActivityManager.RunningTaskInfo taskInfo, boolean hasGlobalFocus,
+                @NonNull Region displayExclusionRegion) {
+            relayout(taskInfo, false /* applyStartTransactionOnDraw */, hasGlobalFocus,
+                    displayExclusionRegion);
         }
 
         @Override
@@ -1085,11 +1093,13 @@
         }
 
         void relayout(ActivityManager.RunningTaskInfo taskInfo,
-                boolean applyStartTransactionOnDraw, boolean hasGlobalFocus) {
+                boolean applyStartTransactionOnDraw, boolean hasGlobalFocus,
+                @NonNull Region displayExclusionRegion) {
             mRelayoutParams.mRunningTaskInfo = taskInfo;
             mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw;
             mRelayoutParams.mLayoutResId = R.layout.caption_layout;
             mRelayoutParams.mHasGlobalFocus = hasGlobalFocus;
+            mRelayoutParams.mDisplayExclusionRegion.set(displayExclusionRegion);
             relayout(mRelayoutParams, mMockSurfaceControlStartT, mMockSurfaceControlFinishT,
                     mMockWindowContainerTransaction, mMockView, mRelayoutResult);
         }
diff --git a/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionException.java b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionException.java
index 28c3b3d..2540236 100644
--- a/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionException.java
+++ b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionException.java
@@ -125,6 +125,7 @@
 
     public AppFunctionException(
             int errorCode, @Nullable String errorMessage, @NonNull Bundle extras) {
+        super(errorMessage);
         mErrorCode = errorCode;
         mErrorMessage = errorMessage;
         mExtras = extras;
diff --git a/libs/hwui/FeatureFlags.h b/libs/hwui/FeatureFlags.h
index fddcf29..5f84f47 100644
--- a/libs/hwui/FeatureFlags.h
+++ b/libs/hwui/FeatureFlags.h
@@ -33,9 +33,9 @@
 #endif  // __ANDROID__
 }
 
-inline bool typeface_redesign() {
+inline bool typeface_redesign_readonly() {
 #ifdef __ANDROID__
-    static bool flag = com_android_text_flags_typeface_redesign();
+    static bool flag = com_android_text_flags_typeface_redesign_readonly();
     return flag;
 #else
     return true;
diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig
index 5ad788c..fa27af6 100644
--- a/libs/hwui/aconfig/hwui_flags.aconfig
+++ b/libs/hwui/aconfig/hwui_flags.aconfig
@@ -154,3 +154,13 @@
   description: "API's that enable animated image drawables to use nearest sampling when scaling."
   bug: "370523334"
 }
+
+flag {
+  name: "remove_vri_sketchy_destroy"
+  namespace: "core_graphics"
+  description: "Remove the eager yet thread-violating destroyHardwareResources in VRI#die"
+  bug: "377057106"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
\ No newline at end of file
diff --git a/libs/hwui/hwui/MinikinUtils.h b/libs/hwui/hwui/MinikinUtils.h
index 1510ce1..20acf98 100644
--- a/libs/hwui/hwui/MinikinUtils.h
+++ b/libs/hwui/hwui/MinikinUtils.h
@@ -73,7 +73,7 @@
     static void forFontRun(const minikin::Layout& layout, Paint* paint, F& f) {
         float saveSkewX = paint->getSkFont().getSkewX();
         bool savefakeBold = paint->getSkFont().isEmbolden();
-        if (text_feature::typeface_redesign()) {
+        if (text_feature::typeface_redesign_readonly()) {
             for (uint32_t runIdx = 0; runIdx < layout.getFontRunCount(); ++runIdx) {
                 uint32_t start = layout.getFontRunStart(runIdx);
                 uint32_t end = layout.getFontRunEnd(runIdx);
diff --git a/libs/hwui/jni/text/TextShaper.cpp b/libs/hwui/jni/text/TextShaper.cpp
index 70e6bed..5f69346 100644
--- a/libs/hwui/jni/text/TextShaper.cpp
+++ b/libs/hwui/jni/text/TextShaper.cpp
@@ -86,7 +86,7 @@
         overallDescent = std::max(overallDescent, extent.descent);
     }
 
-    if (text_feature::typeface_redesign()) {
+    if (text_feature::typeface_redesign_readonly()) {
         uint32_t runCount = layout.getFontRunCount();
 
         std::unordered_map<minikin::FakedFont, uint32_t, FakedFontKey> fakedToFontIds;
@@ -229,7 +229,7 @@
 // CriticalNative
 static jfloat TextShaper_Result_getWeightOverride(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) {
     const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr);
-    if (text_feature::typeface_redesign()) {
+    if (text_feature::typeface_redesign_readonly()) {
         float value =
                 findValueFromVariationSettings(layout->layout.getFakery(i), minikin::TAG_wght);
         return std::isnan(value) ? NO_OVERRIDE : value;
@@ -241,7 +241,7 @@
 // CriticalNative
 static jfloat TextShaper_Result_getItalicOverride(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) {
     const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr);
-    if (text_feature::typeface_redesign()) {
+    if (text_feature::typeface_redesign_readonly()) {
         float value =
                 findValueFromVariationSettings(layout->layout.getFakery(i), minikin::TAG_ital);
         return std::isnan(value) ? NO_OVERRIDE : value;
diff --git a/media/java/android/media/MediaCas.java b/media/java/android/media/MediaCas.java
index 88efed5..3f9126a 100644
--- a/media/java/android/media/MediaCas.java
+++ b/media/java/android/media/MediaCas.java
@@ -1000,7 +1000,10 @@
     @SystemApi
     @RequiresPermission(android.Manifest.permission.TUNER_RESOURCE_ACCESS)
     public boolean updateResourcePriority(int priority, int niceValue) {
-        return mTunerResourceManager.updateClientPriority(mClientId, priority, niceValue);
+        if (mTunerResourceManager != null) {
+            return mTunerResourceManager.updateClientPriority(mClientId, priority, niceValue);
+        }
+        return false;
     }
 
     /**
@@ -1017,7 +1020,9 @@
     @SystemApi
     @RequiresPermission(android.Manifest.permission.TUNER_RESOURCE_ACCESS)
     public void setResourceHolderRetain(boolean resourceHolderRetain) {
-        mTunerResourceManager.setResourceHolderRetain(mClientId, resourceHolderRetain);
+        if (mTunerResourceManager != null) {
+            mTunerResourceManager.setResourceHolderRetain(mClientId, resourceHolderRetain);
+        }
     }
 
     IHwBinder getBinder() {
diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java
index e575dae..2ae89d3 100644
--- a/media/java/android/media/MediaCodec.java
+++ b/media/java/android/media/MediaCodec.java
@@ -18,6 +18,7 @@
 
 import static android.media.codec.Flags.FLAG_NULL_OUTPUT_SURFACE;
 import static android.media.codec.Flags.FLAG_REGION_OF_INTEREST;
+import static android.media.codec.Flags.FLAG_SUBSESSION_METRICS;
 
 import static com.android.media.codec.flags.Flags.FLAG_LARGE_AUDIO_FRAME;
 
@@ -890,7 +891,7 @@
  any start codes), and submit it as a <strong>regular</strong> input buffer.
  <p>
  You will receive an {@link #INFO_OUTPUT_FORMAT_CHANGED} return value from {@link
- #dequeueOutputBuffer dequeueOutputBuffer} or a {@link Callback#onOutputBufferAvailable
+ #dequeueOutputBuffer dequeueOutputBuffer} or a {@link Callback#onOutputFormatChanged
  onOutputFormatChanged} callback just after the picture-size change takes place and before any
  frames with the new size have been returned.
  <p class=note>
@@ -1835,6 +1836,13 @@
     private static final int CB_CRYPTO_ERROR = 6;
     private static final int CB_LARGE_FRAME_OUTPUT_AVAILABLE = 7;
 
+    /**
+     * Callback ID for when the metrics for this codec have been flushed due to
+     * the start of a new subsession. The associated Java Message object will
+     * contain the flushed metrics as a PersistentBundle in the obj field.
+     */
+    private static final int CB_METRICS_FLUSHED = 8;
+
     private class EventHandler extends Handler {
         private MediaCodec mCodec;
 
@@ -2007,6 +2015,15 @@
                     break;
                 }
 
+                case CB_METRICS_FLUSHED:
+                {
+
+                    if (GetFlag(() -> android.media.codec.Flags.subsessionMetrics())) {
+                        mCallback.onMetricsFlushed(mCodec, (PersistableBundle)msg.obj);
+                    }
+                    break;
+                }
+
                 default:
                 {
                     break;
@@ -4958,14 +4975,24 @@
     public native final String getCanonicalName();
 
     /**
-     *  Return Metrics data about the current codec instance.
+     * Return Metrics data about the current codec instance.
+     * <p>
+     * Call this method after configuration, during execution, or after
+     * the codec has been already stopped.
+     * <p>
+     * Beginning with {@link android.os.Build.VERSION_CODES#B}
+     * this method can be used to get the Metrics data prior to an error.
+     * (e.g. in {@link Callback#onError} or after a method throws
+     * {@link MediaCodec.CodecException}.) Before that, the Metrics data was
+     * cleared on error, resulting in a null return value.
      *
      * @return a {@link PersistableBundle} containing the set of attributes and values
      * available for the media being handled by this instance of MediaCodec
      * The attributes are descibed in {@link MetricsConstants}.
      *
      * Additional vendor-specific fields may also be present in
-     * the return value.
+     * the return value. Returns null if there is no Metrics data.
+     *
      */
     public PersistableBundle getMetrics() {
         PersistableBundle bundle = native_getMetrics();
@@ -5692,6 +5719,27 @@
          */
         public abstract void onOutputFormatChanged(
                 @NonNull MediaCodec codec, @NonNull MediaFormat format);
+
+        /**
+         * Called when the metrics for this codec have been flushed due to the
+         * start of a new subsession.
+         * <p>
+         * This can happen when the codec is reconfigured after stop(), or
+         * mid-stream e.g. if the video size changes. When this happens, the
+         * metrics for the previous subsession are flushed, and
+         * {@link MediaCodec#getMetrics} will return the metrics for the
+         * new subsession. This happens just before the {@link Callback#onOutputFormatChanged}
+         * event, so this <b>optional</b> callback is provided to be able to
+         * capture the final metrics for the previous subsession.
+         *
+         * @param codec The MediaCodec object.
+         * @param metrics The flushed metrics for this codec.
+         */
+        @FlaggedApi(FLAG_SUBSESSION_METRICS)
+        public void onMetricsFlushed(
+                @NonNull MediaCodec codec, @NonNull PersistableBundle metrics) {
+            // default implementation ignores this callback.
+        }
     }
 
     private void postEventFromNative(
diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java
index 3499c43..20108e7 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -1771,10 +1771,12 @@
     }
 
     /**
-     * A class to control media routing session in media route provider. For example,
-     * selecting/deselecting/transferring to routes of a session can be done through this. Instances
-     * are created when {@link TransferCallback#onTransfer(RoutingController, RoutingController)} is
-     * called, which is invoked after {@link #transferTo(MediaRoute2Info)} is called.
+     * Controls a media routing session.
+     *
+     * <p>Routing controllers wrap a {@link RoutingSessionInfo}, taking care of mapping route ids to
+     * {@link MediaRoute2Info} instances. You can still access the underlying session using {@link
+     * #getRoutingSessionInfo()}, but keep in mind it can be changed by other threads. Changes to
+     * the routing session are notified via {@link ControllerCallback}.
      */
     public class RoutingController {
         private final Object mControllerLock = new Object();
@@ -1836,7 +1838,9 @@
         }
 
         /**
-         * @return the unmodifiable list of currently selected routes
+         * Returns the unmodifiable list of currently selected routes
+         *
+         * @see RoutingSessionInfo#getSelectedRoutes()
          */
         @NonNull
         public List<MediaRoute2Info> getSelectedRoutes() {
@@ -1848,7 +1852,9 @@
         }
 
         /**
-         * @return the unmodifiable list of selectable routes for the session.
+         * Returns the unmodifiable list of selectable routes for the session.
+         *
+         * @see RoutingSessionInfo#getSelectableRoutes()
          */
         @NonNull
         public List<MediaRoute2Info> getSelectableRoutes() {
@@ -1860,7 +1866,9 @@
         }
 
         /**
-         * @return the unmodifiable list of deselectable routes for the session.
+         * Returns the unmodifiable list of deselectable routes for the session.
+         *
+         * @see RoutingSessionInfo#getDeselectableRoutes()
          */
         @NonNull
         public List<MediaRoute2Info> getDeselectableRoutes() {
diff --git a/media/java/android/media/RoutingSessionInfo.java b/media/java/android/media/RoutingSessionInfo.java
index 83a4dd5..3b8cf3f 100644
--- a/media/java/android/media/RoutingSessionInfo.java
+++ b/media/java/android/media/RoutingSessionInfo.java
@@ -262,7 +262,8 @@
     }
 
     /**
-     * Gets the provider id of the session.
+     * Gets the provider ID of the session.
+     *
      * @hide
      */
     @Nullable
@@ -271,7 +272,15 @@
     }
 
     /**
-     * Gets the list of IDs of selected routes for the session. It shouldn't be empty.
+     * Gets the list of IDs of selected routes for the session.
+     *
+     * <p>Selected routes are the routes that this session is actively routing media to.
+     *
+     * <p>The behavior of a routing session with multiple selected routes is ultimately defined by
+     * the {@link MediaRoute2ProviderService} implementation. However, typically, it's expected that
+     * all the selected routes of a routing session are playing the same media in sync.
+     *
+     * @return A non-empty list of selected route ids.
      */
     @NonNull
     public List<String> getSelectedRoutes() {
@@ -280,6 +289,16 @@
 
     /**
      * Gets the list of IDs of selectable routes for the session.
+     *
+     * <p>Selectable routes can be added to a routing session (via {@link
+     * MediaRouter2.RoutingController#selectRoute}) in order to add them to the {@link
+     * #getSelectedRoutes() selected routes}, so that media plays on the newly selected route along
+     * with the other selected routes.
+     *
+     * <p>Not to be confused with {@link #getTransferableRoutes() transferable routes}. Transferring
+     * to a route makes it the sole selected route.
+     *
+     * @return A possibly empty list of selectable route ids.
      */
     @NonNull
     public List<String> getSelectableRoutes() {
@@ -288,6 +307,17 @@
 
     /**
      * Gets the list of IDs of deselectable routes for the session.
+     *
+     * <p>Deselectable routes can be removed from the {@link #getSelectedRoutes() selected routes},
+     * so that the routing session stops routing to the newly deselected route, but continues on any
+     * remaining selected routes.
+     *
+     * <p>Deselectable routes should be a subset of the {@link #getSelectedRoutes() selected
+     * routes}, meaning not all of the selected routes might be deselectable. For example, one of
+     * the selected routes may be a leader device coordinating group playback, which must always
+     * remain selected while the session is active.
+     *
+     * @return A possibly empty list of deselectable route ids.
      */
     @NonNull
     public List<String> getDeselectableRoutes() {
@@ -296,6 +326,24 @@
 
     /**
      * Gets the list of IDs of transferable routes for the session.
+     *
+     * <p>Transferring to a route (for example, using {@link MediaRouter2#transferTo}) replaces the
+     * list of {@link #getSelectedRoutes() selected routes} with the target route, causing playback
+     * to move from one route to another.
+     *
+     * <p>Note that this is different from {@link #getSelectableRoutes() selectable routes}, because
+     * selecting a route makes it part of the selected routes, while transferring to a route makes
+     * it the selected route. A route can be both transferable and selectable.
+     *
+     * <p>Note that playback may transfer across routes without the target route being in the list
+     * of transferable routes. This can happen by creating a new routing session to the target
+     * route, and releasing the routing session being transferred from. The difference is that a
+     * transfer to a route in the transferable list can happen with no intervention from the app,
+     * with the route provider taking care of the entire operation. A transfer to a route that is
+     * not in the list of transferable routes (by creating a new session) requires the app to move
+     * the playback state from one device to the other.
+     *
+     * @return A possibly empty list of transferable route ids.
      */
     @NonNull
     public List<String> getTransferableRoutes() {
diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig
index ba2398c..7895eb2 100644
--- a/media/java/android/media/flags/media_better_together.aconfig
+++ b/media/java/android/media/flags/media_better_together.aconfig
@@ -166,3 +166,10 @@
     description: "Allows audio input devices routing and volume control via system settings."
     bug: "355684672"
 }
+
+flag {
+    name: "enable_mirroring_in_media_router_2"
+    namespace: "media_better_together"
+    description: "Enables support for mirroring routes in the MediaRouter2 framework, allowing Output Switcher to offer mirroring routes."
+    bug: "362507305"
+}
diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt
index 675c8f8..a23845f 100644
--- a/nfc/api/system-current.txt
+++ b/nfc/api/system-current.txt
@@ -100,6 +100,7 @@
     method public void onHceEventReceived(int);
     method public void onLaunchHceAppChooserActivity(@NonNull String, @NonNull java.util.List<android.nfc.cardemulation.ApduServiceInfo>, @NonNull android.content.ComponentName, @NonNull String);
     method public void onLaunchHceTapAgainDialog(@NonNull android.nfc.cardemulation.ApduServiceInfo, @NonNull String);
+    method public void onLogEventNotified(@NonNull android.nfc.OemLogItems);
     method public void onNdefMessage(@NonNull android.nfc.Tag, @NonNull android.nfc.NdefMessage, @NonNull java.util.function.Consumer<java.lang.Boolean>);
     method public void onNdefRead(@NonNull java.util.function.Consumer<java.lang.Boolean>);
     method public void onReaderOptionChanged(boolean);
@@ -115,6 +116,27 @@
     method public int getNfceeId();
   }
 
+  @FlaggedApi("android.nfc.nfc_oem_extension") public final class OemLogItems implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getAction();
+    method public int getCallingPid();
+    method @Nullable public byte[] getCommandApdu();
+    method public int getEvent();
+    method @Nullable public byte[] getResponseApdu();
+    method @Nullable public java.time.Instant getRfFieldEventTimeMillis();
+    method @Nullable public android.nfc.Tag getTag();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.nfc.OemLogItems> CREATOR;
+    field public static final int EVENT_DISABLE = 2; // 0x2
+    field public static final int EVENT_ENABLE = 1; // 0x1
+    field public static final int EVENT_UNSET = 0; // 0x0
+    field public static final int LOG_ACTION_HCE_DATA = 516; // 0x204
+    field public static final int LOG_ACTION_NFC_TOGGLE = 513; // 0x201
+    field public static final int LOG_ACTION_RF_FIELD_STATE_CHANGED = 1; // 0x1
+    field public static final int LOG_ACTION_SCREEN_STATE_CHANGED = 518; // 0x206
+    field public static final int LOG_ACTION_TAG_DETECTED = 3; // 0x3
+  }
+
   @FlaggedApi("android.nfc.nfc_oem_extension") public class RoutingStatus {
     method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public int getDefaultIsoDepRoute();
     method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public int getDefaultOffHostRoute();
diff --git a/nfc/java/android/nfc/INfcOemExtensionCallback.aidl b/nfc/java/android/nfc/INfcOemExtensionCallback.aidl
index 7f1fd15..b102e87 100644
--- a/nfc/java/android/nfc/INfcOemExtensionCallback.aidl
+++ b/nfc/java/android/nfc/INfcOemExtensionCallback.aidl
@@ -18,6 +18,7 @@
 import android.content.ComponentName;
 import android.nfc.cardemulation.ApduServiceInfo;
 import android.nfc.NdefMessage;
+import android.nfc.OemLogItems;
 import android.nfc.Tag;
 import android.os.ResultReceiver;
 
@@ -51,4 +52,5 @@
    void onNdefMessage(in Tag tag, in NdefMessage message, in ResultReceiver hasOemExecutableContent);
    void onLaunchHceAppChooserActivity(in String selectedAid, in List<ApduServiceInfo> services, in ComponentName failedComponent, in String category);
    void onLaunchHceTapAgainActivity(in ApduServiceInfo service, in String category);
+   void onLogEventNotified(in OemLogItems item);
 }
diff --git a/nfc/java/android/nfc/NfcOemExtension.java b/nfc/java/android/nfc/NfcOemExtension.java
index 1bfe714..abd99bc 100644
--- a/nfc/java/android/nfc/NfcOemExtension.java
+++ b/nfc/java/android/nfc/NfcOemExtension.java
@@ -392,6 +392,12 @@
          * @param category the category of the service
          */
         void onLaunchHceTapAgainDialog(@NonNull ApduServiceInfo service, @NonNull String category);
+
+        /**
+         * Callback when OEM specified log event are notified.
+         * @param item the log items that contains log information of NFC event.
+         */
+        void onLogEventNotified(@NonNull OemLogItems item);
     }
 
 
@@ -900,6 +906,12 @@
                     handleVoid2ArgCallback(service, category, cb::onLaunchHceTapAgainDialog, ex));
         }
 
+        @Override
+        public void onLogEventNotified(OemLogItems item) throws RemoteException  {
+            mCallbackMap.forEach((cb, ex) ->
+                    handleVoidCallback(item, cb::onLogEventNotified, ex));
+        }
+
         private <T> void handleVoidCallback(
                 T input, Consumer<T> callbackMethod, Executor executor) {
             synchronized (mLock) {
diff --git a/nfc/java/android/nfc/OemLogItems.aidl b/nfc/java/android/nfc/OemLogItems.aidl
new file mode 100644
index 0000000..3bcb445
--- /dev/null
+++ b/nfc/java/android/nfc/OemLogItems.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nfc;
+
+parcelable OemLogItems;
\ No newline at end of file
diff --git a/nfc/java/android/nfc/OemLogItems.java b/nfc/java/android/nfc/OemLogItems.java
new file mode 100644
index 0000000..6671941
--- /dev/null
+++ b/nfc/java/android/nfc/OemLogItems.java
@@ -0,0 +1,325 @@
+/*

+ * Copyright 2024 The Android Open Source Project

+ *

+ * Licensed under the Apache License, Version 2.0 (the "License");

+ * you may not use this file except in compliance with the License.

+ * You may obtain a copy of the License at

+ *

+ *      http://www.apache.org/licenses/LICENSE-2.0

+ *

+ * Unless required by applicable law or agreed to in writing, software

+ * distributed under the License is distributed on an "AS IS" BASIS,

+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

+ * See the License for the specific language governing permissions and

+ * limitations under the License.

+ */

+

+package android.nfc;

+

+import android.annotation.FlaggedApi;

+import android.annotation.IntDef;

+import android.annotation.NonNull;

+import android.annotation.Nullable;

+import android.annotation.SystemApi;

+import android.os.Parcel;

+import android.os.Parcelable;

+

+import java.lang.annotation.Retention;

+import java.lang.annotation.RetentionPolicy;

+import java.time.Instant;

+

+/**

+ * A log class for OEMs to get log information of NFC events.

+ * @hide

+ */

+@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)

+@SystemApi

+public final class OemLogItems implements Parcelable {

+    /**

+     * Used when RF field state is changed.

+     */

+    public static final int LOG_ACTION_RF_FIELD_STATE_CHANGED = 0X01;

+    /**

+     * Used when NFC is toggled. Event should be set to {@link LogEvent#EVENT_ENABLE} or

+     * {@link LogEvent#EVENT_DISABLE} if this action is used.

+     */

+    public static final int LOG_ACTION_NFC_TOGGLE = 0x0201;

+    /**

+     * Used when sending host routing status.

+     */

+    public static final int LOG_ACTION_HCE_DATA = 0x0204;

+    /**

+     * Used when screen state is changed.

+     */

+    public static final int LOG_ACTION_SCREEN_STATE_CHANGED = 0x0206;

+    /**

+     * Used when tag is detected.

+     */

+    public static final int LOG_ACTION_TAG_DETECTED = 0x03;

+

+    /**

+     * @hide

+     */

+    @IntDef(prefix = { "LOG_ACTION_" }, value = {

+            LOG_ACTION_RF_FIELD_STATE_CHANGED,

+            LOG_ACTION_NFC_TOGGLE,

+            LOG_ACTION_HCE_DATA,

+            LOG_ACTION_SCREEN_STATE_CHANGED,

+            LOG_ACTION_TAG_DETECTED,

+    })

+    @Retention(RetentionPolicy.SOURCE)

+    public @interface LogAction {}

+

+    /**

+     * Represents the event is not set.

+     */

+    public static final int EVENT_UNSET = 0;

+    /**

+     * Represents nfc enable is called.

+     */

+    public static final int EVENT_ENABLE = 1;

+    /**

+     * Represents nfc disable is called.

+     */

+    public static final int EVENT_DISABLE = 2;

+    /** @hide */

+    @IntDef(prefix = { "EVENT_" }, value = {

+            EVENT_UNSET,

+            EVENT_ENABLE,

+            EVENT_DISABLE,

+    })

+    @Retention(RetentionPolicy.SOURCE)

+    public @interface LogEvent {}

+    private int mAction;

+    private int mEvent;

+    private int mCallingPid;

+    private byte[] mCommandApdus;

+    private byte[] mResponseApdus;

+    private Instant mRfFieldOnTime;

+    private Tag mTag;

+

+    /** @hide */

+    public OemLogItems(@LogAction int action, @LogEvent int event, int callingPid,

+            byte[] commandApdus, byte[] responseApdus, Instant rfFieldOnTime,

+            Tag tag) {

+        mAction = action;

+        mEvent = event;

+        mTag = tag;

+        mCallingPid = callingPid;

+        mCommandApdus = commandApdus;

+        mResponseApdus = responseApdus;

+        mRfFieldOnTime = rfFieldOnTime;

+    }

+

+    /**

+     * Describe the kinds of special objects contained in this Parcelable

+     * instance's marshaled representation. For example, if the object will

+     * include a file descriptor in the output of {@link #writeToParcel(Parcel, int)},

+     * the return value of this method must include the

+     * {@link #CONTENTS_FILE_DESCRIPTOR} bit.

+     *

+     * @return a bitmask indicating the set of special object types marshaled

+     * by this Parcelable object instance.

+     */

+    @Override

+    public int describeContents() {

+        return 0;

+    }

+

+    /**

+     * Flatten this object in to a Parcel.

+     *

+     * @param dest  The Parcel in which the object should be written.

+     * @param flags Additional flags about how the object should be written.

+     *              May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}.

+     */

+    @Override

+    public void writeToParcel(@NonNull Parcel dest, int flags) {

+        dest.writeInt(mAction);

+        dest.writeInt(mEvent);

+        dest.writeInt(mCallingPid);

+        dest.writeInt(mCommandApdus.length);

+        dest.writeByteArray(mCommandApdus);

+        dest.writeInt(mResponseApdus.length);

+        dest.writeByteArray(mResponseApdus);

+        dest.writeLong(mRfFieldOnTime.getEpochSecond());

+        dest.writeInt(mRfFieldOnTime.getNano());

+        dest.writeParcelable(mTag, 0);

+    }

+

+    /** @hide */

+    public static class Builder {

+        private final OemLogItems mItem;

+

+        public Builder(@LogAction int type) {

+            mItem = new OemLogItems(type, EVENT_UNSET, 0, new byte[0], new byte[0], null, null);

+        }

+

+        /** Setter of the log action. */

+        public OemLogItems.Builder setAction(@LogAction int action) {

+            mItem.mAction = action;

+            return this;

+        }

+

+        /** Setter of the log calling event. */

+        public OemLogItems.Builder setCallingEvent(@LogEvent int event) {

+            mItem.mEvent = event;

+            return this;

+        }

+

+        /** Setter of the log calling Pid. */

+        public OemLogItems.Builder setCallingPid(int pid) {

+            mItem.mCallingPid = pid;

+            return this;

+        }

+

+        /** Setter of APDU command. */

+        public OemLogItems.Builder setApduCommand(byte[] apdus) {

+            mItem.mCommandApdus = apdus;

+            return this;

+        }

+

+        /** Setter of RF field on time. */

+        public OemLogItems.Builder setRfFieldOnTime(Instant time) {

+            mItem.mRfFieldOnTime = time;

+            return this;

+        }

+

+        /** Setter of APDU response. */

+        public OemLogItems.Builder setApduResponse(byte[] apdus) {

+            mItem.mResponseApdus = apdus;

+            return this;

+        }

+

+        /** Setter of dispatched tag. */

+        public OemLogItems.Builder setTag(Tag tag) {

+            mItem.mTag = tag;

+            return this;

+        }

+

+        /** Builds an {@link OemLogItems} instance. */

+        public OemLogItems build() {

+            return mItem;

+        }

+    }

+

+    /**

+     * Gets the action of this log.

+     * @return one of {@link LogAction}

+     */

+    @LogAction

+    public int getAction() {

+        return mAction;

+    }

+

+    /**

+     * Gets the event of this log. This will be set to {@link LogEvent#EVENT_ENABLE} or

+     * {@link LogEvent#EVENT_DISABLE} only when action is set to

+     * {@link LogAction#LOG_ACTION_NFC_TOGGLE}

+     * @return one of {@link LogEvent}

+     */

+    @LogEvent

+    public int getEvent() {

+        return mEvent;

+    }

+

+    /**

+     * Gets the calling Pid of this log. This field will be set only when action is set to

+     * {@link LogAction#LOG_ACTION_NFC_TOGGLE}

+     * @return calling Pid

+     */

+    public int getCallingPid() {

+        return mCallingPid;

+    }

+

+    /**

+     * Gets the command APDUs of this log. This field will be set only when action is set to

+     * {@link LogAction#LOG_ACTION_HCE_DATA}

+     * @return a byte array of command APDUs with the same format as

+     * {@link android.nfc.cardemulation.HostApduService#sendResponseApdu(byte[])}

+     */

+    @Nullable

+    public byte[] getCommandApdu() {

+        return mCommandApdus;

+    }

+

+    /**

+     * Gets the response APDUs of this log. This field will be set only when action is set to

+     * {@link LogAction#LOG_ACTION_HCE_DATA}

+     * @return a byte array of response APDUs with the same format as

+     * {@link android.nfc.cardemulation.HostApduService#sendResponseApdu(byte[])}

+     */

+    @Nullable

+    public byte[] getResponseApdu() {

+        return mResponseApdus;

+    }

+

+    /**

+     * Gets the RF field event time in this log in millisecond. This field will be set only when

+     * action is set to {@link LogAction#LOG_ACTION_RF_FIELD_STATE_CHANGED}

+     * @return an {@link Instant} of RF field event time.

+     */

+    @Nullable

+    public Instant getRfFieldEventTimeMillis() {

+        return mRfFieldOnTime;

+    }

+

+    /**

+     * Gets the tag of this log. This field will be set only when action is set to

+     * {@link LogAction#LOG_ACTION_TAG_DETECTED}

+     * @return a detected {@link Tag} in {@link #LOG_ACTION_TAG_DETECTED} case. Return

+     * null otherwise.

+     */

+    @Nullable

+    public Tag getTag() {

+        return mTag;

+    }

+

+    private String byteToHex(byte[] bytes) {

+        char[] HexArray = "0123456789ABCDEF".toCharArray();

+        char[] hexChars = new char[bytes.length * 2];

+        for (int j = 0; j < bytes.length; j++) {

+            int v = bytes[j] & 0xFF;

+            hexChars[j * 2] = HexArray[v >>> 4];

+            hexChars[j * 2 + 1] = HexArray[v & 0x0F];

+        }

+        return new String(hexChars);

+    }

+

+    @Override

+    public String toString() {

+        return "[mCommandApdus: "

+                + ((mCommandApdus != null) ? byteToHex(mCommandApdus) : "null")

+                + "[mResponseApdus: "

+                + ((mResponseApdus != null) ? byteToHex(mResponseApdus) : "null")

+                + ", mCallingApi= " + mEvent

+                + ", mAction= " + mAction

+                + ", mCallingPId = " + mCallingPid

+                + ", mRfFieldOnTime= " + mRfFieldOnTime;

+    }

+    private OemLogItems(Parcel in) {

+        this.mAction = in.readInt();

+        this.mEvent = in.readInt();

+        this.mCallingPid = in.readInt();

+        this.mCommandApdus = new byte[in.readInt()];

+        in.readByteArray(this.mCommandApdus);

+        this.mResponseApdus = new byte[in.readInt()];

+        in.readByteArray(this.mResponseApdus);

+        this.mRfFieldOnTime = Instant.ofEpochSecond(in.readLong(), in.readInt());

+        this.mTag = in.readParcelable(Tag.class.getClassLoader(), Tag.class);

+    }

+

+    public static final @NonNull Parcelable.Creator<OemLogItems> CREATOR =

+            new Parcelable.Creator<OemLogItems>() {

+                @Override

+                public OemLogItems createFromParcel(Parcel in) {

+                    return new OemLogItems(in);

+                }

+

+                @Override

+                public OemLogItems[] newArray(int size) {

+                    return new OemLogItems[size];

+                }

+            };

+

+}

diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt
index 843d2aa..cd03dd7 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt
@@ -202,6 +202,12 @@
             entry.value.execute { observer.onKeyChanged(key, reason) }
         }
     }
+
+    fun hasAnyObserver(): Boolean {
+        synchronized(observers) { if (observers.isNotEmpty()) return true }
+        synchronized(keyedObservers) { if (keyedObservers.isNotEmpty()) return true }
+        return false
+    }
 }
 
 /** [KeyedObservable] with no-op implementations for all interfaces. */
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt
index 94d373b..bde4217 100644
--- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt
@@ -136,6 +136,18 @@
         for (it in children) action(it)
     }
 
+    /** Traversals preference hierarchy recursively and applies given action. */
+    fun forEachRecursively(action: (PreferenceHierarchyNode) -> Unit) {
+        action(this)
+        for (child in children) {
+            if (child is PreferenceHierarchy) {
+                child.forEachRecursively(action)
+            } else {
+                action(child)
+            }
+        }
+    }
+
     /** Traversals preference hierarchy and applies given action. */
     suspend fun forEachAsync(action: suspend (PreferenceHierarchyNode) -> Unit) {
         for (it in children) action(it)
@@ -157,18 +169,7 @@
 
     /** Returns all the [PreferenceHierarchyNode]s appear in the hierarchy. */
     fun getAllPreferences(): List<PreferenceHierarchyNode> =
-        mutableListOf<PreferenceHierarchyNode>().also { getAllPreferences(it) }
-
-    private fun getAllPreferences(result: MutableList<PreferenceHierarchyNode>) {
-        result.add(this)
-        for (child in children) {
-            if (child is PreferenceHierarchy) {
-                child.getAllPreferences(result)
-            } else {
-                result.add(child)
-            }
-        }
-    }
+        mutableListOf<PreferenceHierarchyNode>().apply { forEachRecursively { add(it) } }
 }
 
 /**
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt
index 41a626f..991d5b7 100644
--- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt
@@ -32,7 +32,7 @@
 open class PreferenceFragment :
     SettingsBasePreferenceFragment(), PreferenceScreenProvider, PreferenceScreenBindingKeyProvider {
 
-    private var preferenceScreenBindingHelper: PreferenceScreenBindingHelper? = null
+    protected var preferenceScreenBindingHelper: PreferenceScreenBindingHelper? = null
 
     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
         preferenceScreen = createPreferenceScreen()
@@ -129,7 +129,9 @@
     }
 
     protected fun getPreferenceKeysInHierarchy(): Set<String> =
-        preferenceScreenBindingHelper?.getPreferences()?.map { it.metadata.key }?.toSet() ?: setOf()
+        preferenceScreenBindingHelper?.let {
+            mutableSetOf<String>().apply { it.forEachRecursively { add(it.metadata.key) } }
+        } ?: setOf()
 
     companion object {
         private const val TAG = "PreferenceFragment"
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
index 022fb1d..fbe8927 100644
--- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
@@ -143,7 +143,8 @@
         }
     }
 
-    fun getPreferences() = preferenceHierarchy.getAllPreferences()
+    fun forEachRecursively(action: (PreferenceHierarchyNode) -> Unit) =
+        preferenceHierarchy.forEachRecursively(action)
 
     fun onCreate() {
         for (preference in lifecycleAwarePreferences) {
@@ -191,11 +192,11 @@
 
     companion object {
         /** Preference value is changed. */
-        private const val CHANGE_REASON_VALUE = 0
+        const val CHANGE_REASON_VALUE = 0
         /** Preference state (title/summary, enable state, etc.) is changed. */
-        private const val CHANGE_REASON_STATE = 1
+        const val CHANGE_REASON_STATE = 1
         /** Dependent preference state is changed. */
-        private const val CHANGE_REASON_DEPENDENT = 2
+        const val CHANGE_REASON_DEPENDENT = 2
 
         /** Updates preference screen that has incomplete hierarchy. */
         @JvmStatic
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
index edd49c5..0209eb8 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
@@ -21,6 +21,7 @@
 import android.content.Context
 import android.content.Intent
 import android.content.ServiceConnection
+import android.os.DeadObjectException
 import android.os.IBinder
 import android.os.IInterface
 import android.os.RemoteException
@@ -52,6 +53,7 @@
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.catch
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.emitAll
 import kotlinx.coroutines.flow.filterIsInstance
@@ -304,6 +306,14 @@
                 service.registerDeviceSettingsListener(deviceInfo, listener)
                 awaitClose { service.unregisterDeviceSettingsListener(deviceInfo, listener) }
             }
+            .catch { e ->
+                if (e is DeadObjectException) {
+                    Log.e(TAG, "DeadObjectException happens when registering listener.", e)
+                    emit(listOf())
+                } else {
+                    throw e
+                }
+            }
             .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())
     }
 
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index 064198f..927a1c59 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -284,5 +284,6 @@
         Settings.Secure.MANDATORY_BIOMETRICS,
         Settings.Secure.MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED,
         Settings.Secure.ADVANCED_PROTECTION_MODE,
+        Settings.Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS,
     };
 }
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index c002a04..6d73ee2 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -332,6 +332,9 @@
         VALIDATORS.put(
                 Secure.ACCESSIBILITY_QS_TARGETS,
                 ACCESSIBILITY_SHORTCUT_TARGET_LIST_VALIDATOR);
+        VALIDATORS.put(
+                Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS,
+                ACCESSIBILITY_SHORTCUT_TARGET_LIST_VALIDATOR);
         VALIDATORS.put(Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.ONE_HANDED_MODE_ACTIVATED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.ONE_HANDED_MODE_ENABLED, BOOLEAN_VALIDATOR);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index 2034f36..fb0aaf8 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -1823,6 +1823,9 @@
                 Settings.Secure.ACCESSIBILITY_QS_TARGETS,
                 SecureSettingsProto.Accessibility.QS_TARGETS);
         dumpSetting(s, p,
+                Settings.Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS,
+                SecureSettingsProto.Accessibility.ACCESSIBILITY_KEY_GESTURE_TARGETS);
+        dumpSetting(s, p,
                 Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY,
                 SecureSettingsProto.Accessibility.ACCESSIBILITY_MAGNIFICATION_CAPABILITY);
         dumpSetting(s, p,
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 0724410..7b6321d 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -962,6 +962,10 @@
     <!-- Permission required for ExecutableMethodFileOffsetsTest -->
     <uses-permission android:name="android.permission.DYNAMIC_INSTRUMENTATION" />
 
+    <!-- Permissions required for CTS test - SettingsPreferenceServiceClientTest -->
+    <uses-permission android:name="android.permission.READ_SYSTEM_PREFERENCES" />
+    <uses-permission android:name="android.permission.WRITE_SYSTEM_PREFERENCES" />
+
     <application
         android:label="@string/app_label"
         android:theme="@android:style/Theme.DeviceDefault.DayNight"
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 207ed71..3df9603 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -267,7 +267,7 @@
 flag {
     name: "dual_shade"
     namespace: "systemui"
-    description: "Enables the BC25 Dual Shade (go/bc25-dual-shade-design)."
+    description: "Enables Dual Shade (go/dual-shade-design-doc)."
     bug: "337259436"
 }
 
@@ -1360,16 +1360,6 @@
 }
 
 flag {
-  name: "notification_pulsing_fix"
-  namespace: "systemui"
-  description: "Allow showing new pulsing notifications when the device is already pulsing."
-  bug: "335560575"
-  metadata {
-    purpose: PURPOSE_BUGFIX
-  }
-}
-
-flag {
     name: "media_lockscreen_launch_animation"
     namespace : "systemui"
     description : "Enable the origin launch animation for UMO when opening on top of lockscreen."
@@ -1784,3 +1774,13 @@
       purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "keyguard_transition_force_finish_on_screen_off"
+    namespace: "systemui"
+    description: "Forces KTF transitions to finish if the screen turns all the way off."
+    bug: "331636736"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/customization/res/values-sw600dp-land/dimens.xml b/packages/SystemUI/customization/res/values-sw600dp-land/dimens.xml
new file mode 100644
index 0000000..651e401
--- /dev/null
+++ b/packages/SystemUI/customization/res/values-sw600dp-land/dimens.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <dimen name="keyguard_smartspace_top_offset">0dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/customization/res/values-sw600dp/dimens.xml b/packages/SystemUI/customization/res/values-sw600dp/dimens.xml
new file mode 100644
index 0000000..10e630d
--- /dev/null
+++ b/packages/SystemUI/customization/res/values-sw600dp/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <!-- For portrait direction in unfold foldable device, we don't need keyguard_smartspace_top_offset-->
+    <dimen name="keyguard_smartspace_top_offset">0dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/customization/res/values/dimens.xml b/packages/SystemUI/customization/res/values/dimens.xml
index c574d1f..7feea6e 100644
--- a/packages/SystemUI/customization/res/values/dimens.xml
+++ b/packages/SystemUI/customization/res/values/dimens.xml
@@ -33,4 +33,10 @@
     <dimen name="small_clock_height">114dp</dimen>
     <dimen name="small_clock_padding_top">28dp</dimen>
     <dimen name="clock_padding_start">28dp</dimen>
+
+    <!-- When large clock is showing, offset the smartspace by this amount -->
+    <dimen name="keyguard_smartspace_top_offset">12dp</dimen>
+    <!--Dimens used in both lockscreen  preview and smartspace -->
+    <dimen name="date_weather_view_height">24dp</dimen>
+    <dimen name="enhanced_smartspace_height">104dp</dimen>
 </resources>
\ No newline at end of file
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
index a4782ac..ee21ea6 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
@@ -55,10 +55,7 @@
     override val view: View
         get() = layerController.view
 
-    override val config =
-        ClockFaceConfig(
-            hasCustomPositionUpdatedAnimation = false // TODO(b/364673982)
-        )
+    override val config = ClockFaceConfig(hasCustomPositionUpdatedAnimation = true)
 
     override var theme = ThemeConfig(true, assets.seedColor)
 
@@ -96,6 +93,19 @@
         layerController.view.layoutParams = lp
     }
 
+    /** See documentation at [FlexClockView.offsetGlyphsForStepClockAnimation]. */
+    private fun offsetGlyphsForStepClockAnimation(
+        clockStartLeft: Int,
+        direction: Int,
+        fraction: Float
+    ) {
+        (view as? FlexClockView)?.offsetGlyphsForStepClockAnimation(
+            clockStartLeft,
+            direction,
+            fraction,
+        )
+    }
+
     override val layout: ClockFaceLayout =
         DefaultClockFaceLayout(view).apply {
             views[0].id =
@@ -248,10 +258,12 @@
 
             override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {
                 layerController.animations.onPositionUpdated(fromLeft, direction, fraction)
+                if (isLargeClock) offsetGlyphsForStepClockAnimation(fromLeft, direction, fraction)
             }
 
             override fun onPositionUpdated(distance: Float, fraction: Float) {
                 layerController.animations.onPositionUpdated(distance, fraction)
+                // TODO(b/378128811) port stepping animation
             }
         }
 }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
index d86c0d6..593eba9 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import android.graphics.Canvas
 import android.graphics.Point
+import android.util.MathUtils.constrainedMap
 import android.view.View
 import android.view.ViewGroup
 import android.widget.RelativeLayout
@@ -50,6 +51,8 @@
             )
     }
 
+    private val digitOffsets = mutableMapOf<Int, Float>()
+
     override fun addView(child: View?) {
         super.addView(child)
         (child as SimpleDigitalClockTextView).digitTranslateAnimator =
@@ -76,7 +79,7 @@
         digitLeftTopMap[R.id.HOUR_SECOND_DIGIT] = Point(maxSingleDigitSize.x, 0)
         digitLeftTopMap[R.id.MINUTE_FIRST_DIGIT] = Point(0, maxSingleDigitSize.y)
         digitLeftTopMap[R.id.MINUTE_SECOND_DIGIT] = Point(maxSingleDigitSize)
-        digitLeftTopMap.forEach { _, point ->
+        digitLeftTopMap.forEach { (_, point) ->
             point.x += abs(aodTranslate.x)
             point.y += abs(aodTranslate.y)
         }
@@ -89,11 +92,17 @@
 
     override fun onDraw(canvas: Canvas) {
         super.onDraw(canvas)
-        digitalClockTextViewMap.forEach { (id, _) ->
-            val textView = digitalClockTextViewMap[id]!!
-            canvas.translate(digitLeftTopMap[id]!!.x.toFloat(), digitLeftTopMap[id]!!.y.toFloat())
+        digitalClockTextViewMap.forEach { (id, textView) ->
+            // save canvas location in anticipation of restoration later
+            canvas.save()
+            val xTranslateAmount =
+                digitOffsets.getOrDefault(id, 0f) + digitLeftTopMap[id]!!.x.toFloat()
+            // move canvas to location that the textView would like
+            canvas.translate(xTranslateAmount, digitLeftTopMap[id]!!.y.toFloat())
+            // draw the textView at the location of the canvas above
             textView.draw(canvas)
-            canvas.translate(-digitLeftTopMap[id]!!.x.toFloat(), -digitLeftTopMap[id]!!.y.toFloat())
+            // reset the canvas location back to 0 without drawing
+            canvas.restore()
         }
     }
 
@@ -157,10 +166,108 @@
         }
     }
 
+    /**
+     * Offsets the textViews of the clock for the step clock animation.
+     *
+     * The animation makes the textViews of the clock move at different speeds, when the clock is
+     * moving horizontally.
+     *
+     * @param clockStartLeft the [getLeft] position of the clock, before it started moving.
+     * @param clockMoveDirection the direction in which it is moving. A positive number means right,
+     *   and negative means left.
+     * @param moveFraction fraction of the clock movement. 0 means it is at the beginning, and 1
+     *   means it finished moving.
+     */
+    fun offsetGlyphsForStepClockAnimation(
+        clockStartLeft: Int,
+        clockMoveDirection: Int,
+        moveFraction: Float,
+    ) {
+        val isMovingToCenter = if (isLayoutRtl) clockMoveDirection < 0 else clockMoveDirection > 0
+        // The sign of moveAmountDeltaForDigit is already set here
+        // we can interpret (left - clockStartLeft) as (destinationPosition - originPosition)
+        // so we no longer need to multiply direct sign to moveAmountDeltaForDigit
+        val currentMoveAmount = left - clockStartLeft
+        for (i in 0 until NUM_DIGITS) {
+            val mapIndexToId =
+                when (i) {
+                    0 -> R.id.HOUR_FIRST_DIGIT
+                    1 -> R.id.HOUR_SECOND_DIGIT
+                    2 -> R.id.MINUTE_FIRST_DIGIT
+                    3 -> R.id.MINUTE_SECOND_DIGIT
+                    else -> -1
+                }
+            val digitFraction =
+                getDigitFraction(
+                    digit = i,
+                    isMovingToCenter = isMovingToCenter,
+                    fraction = moveFraction,
+                )
+            // left here is the final left position after the animation is done
+            val moveAmountForDigit = currentMoveAmount * digitFraction
+            var moveAmountDeltaForDigit = moveAmountForDigit - currentMoveAmount
+            if (isMovingToCenter && moveAmountForDigit < 0) moveAmountDeltaForDigit *= -1
+            digitOffsets[mapIndexToId] = moveAmountDeltaForDigit
+            invalidate()
+        }
+    }
+
+    private val moveToCenterDelays: List<Int>
+        get() = if (isLayoutRtl) MOVE_LEFT_DELAYS else MOVE_RIGHT_DELAYS
+
+    private val moveToSideDelays: List<Int>
+        get() = if (isLayoutRtl) MOVE_RIGHT_DELAYS else MOVE_LEFT_DELAYS
+
+    private fun getDigitFraction(digit: Int, isMovingToCenter: Boolean, fraction: Float): Float {
+        // The delay for the digit, in terms of fraction.
+        // (i.e. the digit should not move during 0.0 - 0.1).
+        val delays = if (isMovingToCenter) moveToCenterDelays else moveToSideDelays
+        val digitInitialDelay = delays[digit] * MOVE_DIGIT_STEP
+        return MOVE_INTERPOLATOR.getInterpolation(
+            constrainedMap(
+                /* rangeMin= */ 0.0f,
+                /* rangeMax= */ 1.0f,
+                /* valueMin= */ digitInitialDelay,
+                /* valueMax= */ digitInitialDelay + AVAILABLE_ANIMATION_TIME,
+                /* value= */ fraction,
+            )
+        )
+    }
+
     companion object {
         val AOD_TRANSITION_DURATION = 750L
         val CHARGING_TRANSITION_DURATION = 300L
 
+        // Calculate the positions of all of the digits...
+        // Offset each digit by, say, 0.1
+        // This means that each digit needs to move over a slice of "fractions", i.e. digit 0 should
+        // move from 0.0 - 0.7, digit 1 from 0.1 - 0.8, digit 2 from 0.2 - 0.9, and digit 3
+        // from 0.3 - 1.0.
+        private const val NUM_DIGITS = 4
+
+        // Delays. Each digit's animation should have a slight delay, so we get a nice
+        // "stepping" effect. When moving right, the second digit of the hour should move first.
+        // When moving left, the first digit of the hour should move first. The lists encode
+        // the delay for each digit (hour[0], hour[1], minute[0], minute[1]), to be multiplied
+        // by delayMultiplier.
+        private val MOVE_LEFT_DELAYS = listOf(0, 1, 2, 3)
+        private val MOVE_RIGHT_DELAYS = listOf(1, 0, 3, 2)
+
+        // How much delay to apply to each subsequent digit. This is measured in terms of "fraction"
+        // (i.e. a value of 0.1 would cause a digit to wait until fraction had hit 0.1, or 0.2 etc
+        // before moving).
+        //
+        // The current specs dictate that each digit should have a 33ms gap between them. The
+        // overall time is 1s right now.
+        private const val MOVE_DIGIT_STEP = 0.033f
+
+        // Constants for the animation
+        private val MOVE_INTERPOLATOR = Interpolators.EMPHASIZED
+
+        // Total available transition time for each digit, taking into account the step. If step is
+        // 0.1, then digit 0 would animate over 0.0 - 0.7, making availableTime 0.7.
+        private const val AVAILABLE_ANIMATION_TIME = 1.0f - MOVE_DIGIT_STEP * (NUM_DIGITS - 1)
+
         // Use the sign of targetTranslation to control the direction of digit translation
         fun updateDirectionalTargetTranslate(id: Int, targetTranslation: Point): Point {
             val outPoint = Point(targetTranslation)
@@ -169,17 +276,14 @@
                     outPoint.x *= -1
                     outPoint.y *= -1
                 }
-
                 R.id.HOUR_SECOND_DIGIT -> {
                     outPoint.x *= 1
                     outPoint.y *= -1
                 }
-
                 R.id.MINUTE_FIRST_DIGIT -> {
                     outPoint.x *= -1
                     outPoint.y *= 1
                 }
-
                 R.id.MINUTE_SECOND_DIGIT -> {
                     outPoint.x *= 1
                     outPoint.y *= 1
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt
index e142169..58fe2c9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt
@@ -36,6 +36,7 @@
 
 private const val USER_ID = 22
 private const val OWNER_ID = 10
+private const val PASSWORD_ID = 30
 private const val OPERATION_ID = 100L
 private const val MAX_ATTEMPTS = 5
 
@@ -247,7 +248,11 @@
 private fun pinRequest(credentialOwner: Int = USER_ID): BiometricPromptRequest.Credential.Pin =
     BiometricPromptRequest.Credential.Pin(
         promptInfo(),
-        BiometricUserInfo(userId = USER_ID, deviceCredentialOwnerId = credentialOwner),
+        BiometricUserInfo(
+            userId = USER_ID,
+            deviceCredentialOwnerId = credentialOwner,
+            userIdForPasswordEntry = PASSWORD_ID,
+        ),
         BiometricOperationInfo(OPERATION_ID),
     )
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
index 81d3f72..f0d79bb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.deviceentry.domain.interactor
 
 import android.content.pm.UserInfo
+import android.os.PowerManager
+import android.provider.Settings
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.widget.LockPatternUtils
@@ -31,6 +33,7 @@
 import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeTrustRepository
 import com.android.systemui.keyguard.shared.model.AuthenticationFlags
 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
@@ -38,12 +41,18 @@
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
 import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.testKosmos
 import com.android.systemui.user.data.model.SelectionStatus
 import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
+import com.android.systemui.util.settings.fakeSettings
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -225,6 +234,8 @@
     @Test
     fun deviceUnlockStatus_isResetToFalse_whenDeviceGoesToSleep() =
         testScope.runTest {
+            setLockAfterScreenTimeout(0)
+            kosmos.fakeAuthenticationRepository.powerButtonInstantlyLocks = false
             val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus)
 
             kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
@@ -240,8 +251,52 @@
         }
 
     @Test
+    fun deviceUnlockStatus_isResetToFalse_whenDeviceGoesToSleep_afterDelay() =
+        testScope.runTest {
+            val delay = 5000
+            setLockAfterScreenTimeout(delay)
+            kosmos.fakeAuthenticationRepository.powerButtonInstantlyLocks = false
+            val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus)
+
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                SuccessFingerprintAuthenticationStatus(0, true)
+            )
+            runCurrent()
+            assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
+
+            kosmos.powerInteractor.setAsleepForTest()
+            runCurrent()
+            assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
+
+            advanceTimeBy(delay.toLong())
+            assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
+        }
+
+    @Test
+    fun deviceUnlockStatus_isResetToFalse_whenDeviceGoesToSleep_powerButtonLocksInstantly() =
+        testScope.runTest {
+            setLockAfterScreenTimeout(5000)
+            kosmos.fakeAuthenticationRepository.powerButtonInstantlyLocks = true
+            val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus)
+
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                SuccessFingerprintAuthenticationStatus(0, true)
+            )
+            runCurrent()
+            assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
+
+            kosmos.powerInteractor.setAsleepForTest(
+                sleepReason = PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON
+            )
+            runCurrent()
+
+            assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
+        }
+
+    @Test
     fun deviceUnlockStatus_becomesUnlocked_whenFingerprintUnlocked_whileDeviceAsleep() =
         testScope.runTest {
+            setLockAfterScreenTimeout(0)
             val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus)
             assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
 
@@ -450,6 +505,98 @@
                 .isEqualTo(DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate)
         }
 
+    @Test
+    fun deviceUnlockStatus_locksImmediately_whenDreamStarts_noTimeout() =
+        testScope.runTest {
+            setLockAfterScreenTimeout(0)
+            val isUnlocked by collectLastValue(underTest.deviceUnlockStatus.map { it.isUnlocked })
+            unlockDevice()
+
+            startDreaming()
+
+            assertThat(isUnlocked).isFalse()
+        }
+
+    @Test
+    fun deviceUnlockStatus_locksWithDelay_afterDreamStarts_withTimeout() =
+        testScope.runTest {
+            val delay = 5000
+            setLockAfterScreenTimeout(delay)
+            val isUnlocked by collectLastValue(underTest.deviceUnlockStatus.map { it.isUnlocked })
+            unlockDevice()
+
+            startDreaming()
+            assertThat(isUnlocked).isTrue()
+
+            advanceTimeBy(delay - 1L)
+            assertThat(isUnlocked).isTrue()
+
+            advanceTimeBy(1L)
+            assertThat(isUnlocked).isFalse()
+        }
+
+    @Test
+    fun deviceUnlockStatus_doesNotLockWithDelay_whenDreamStopsBeforeTimeout() =
+        testScope.runTest {
+            val delay = 5000
+            setLockAfterScreenTimeout(delay)
+            val isUnlocked by collectLastValue(underTest.deviceUnlockStatus.map { it.isUnlocked })
+            unlockDevice()
+
+            startDreaming()
+            assertThat(isUnlocked).isTrue()
+
+            advanceTimeBy(delay - 1L)
+            assertThat(isUnlocked).isTrue()
+
+            stopDreaming()
+            assertThat(isUnlocked).isTrue()
+
+            advanceTimeBy(1L)
+            assertThat(isUnlocked).isTrue()
+        }
+
+    @Test
+    fun deviceUnlockStatus_doesNotLock_whenDreamStarts_ifNotInteractive() =
+        testScope.runTest {
+            setLockAfterScreenTimeout(0)
+            val isUnlocked by collectLastValue(underTest.deviceUnlockStatus.map { it.isUnlocked })
+            unlockDevice()
+
+            startDreaming()
+
+            assertThat(isUnlocked).isFalse()
+        }
+
+    private fun TestScope.unlockDevice() {
+        val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus)
+
+        kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+            SuccessFingerprintAuthenticationStatus(0, true)
+        )
+        assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
+        kosmos.sceneInteractor.changeScene(Scenes.Gone, "reason")
+        runCurrent()
+    }
+
+    private fun setLockAfterScreenTimeout(timeoutMs: Int) {
+        kosmos.fakeSettings.putIntForUser(
+            Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT,
+            timeoutMs,
+            kosmos.selectedUserInteractor.getSelectedUserId(),
+        )
+    }
+
+    private fun TestScope.startDreaming() {
+        kosmos.fakeKeyguardRepository.setDreaming(true)
+        runCurrent()
+    }
+
+    private fun TestScope.stopDreaming() {
+        kosmos.fakeKeyguardRepository.setDreaming(false)
+        runCurrent()
+    }
+
     private fun TestScope.verifyRestrictionReasonsForAuthFlags(
         vararg authFlagToDeviceEntryRestriction: Pair<Int, DeviceEntryRestrictionReason?>
     ) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
index bfe89de..3d5498b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
@@ -16,11 +16,14 @@
 
 package com.android.systemui.keyguard.data.repository
 
+import android.animation.Animator
 import android.animation.ValueAnimator
+import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.SmallTest
 import com.android.app.animation.Interpolators
+import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -41,6 +44,8 @@
 import java.math.BigDecimal
 import java.math.RoundingMode
 import java.util.UUID
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.dropWhile
@@ -53,6 +58,7 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -65,6 +71,8 @@
     private lateinit var underTest: KeyguardTransitionRepository
     private lateinit var runner: KeyguardTransitionRunner
 
+    private val animatorListener = mock<Animator.AnimatorListener>()
+
     @Before
     fun setUp() {
         underTest = KeyguardTransitionRepositoryImpl(Dispatchers.Main)
@@ -80,7 +88,7 @@
             runner.startTransition(
                 this,
                 TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, getAnimator()),
-                maxFrames = 100
+                maxFrames = 100,
             )
 
             assertSteps(steps, listWithStep(BigDecimal(.1)), AOD, LOCKSCREEN)
@@ -107,7 +115,7 @@
                     LOCKSCREEN,
                     AOD,
                     getAnimator(),
-                    TransitionModeOnCanceled.LAST_VALUE
+                    TransitionModeOnCanceled.LAST_VALUE,
                 ),
             )
 
@@ -142,7 +150,7 @@
                     LOCKSCREEN,
                     AOD,
                     getAnimator(),
-                    TransitionModeOnCanceled.RESET
+                    TransitionModeOnCanceled.RESET,
                 ),
             )
 
@@ -177,7 +185,7 @@
                     LOCKSCREEN,
                     AOD,
                     getAnimator(),
-                    TransitionModeOnCanceled.REVERSE
+                    TransitionModeOnCanceled.REVERSE,
                 ),
             )
 
@@ -476,6 +484,49 @@
             assertThat(steps.size).isEqualTo(3)
         }
 
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_TRANSITION_FORCE_FINISH_ON_SCREEN_OFF)
+    fun forceFinishCurrentTransition_noFurtherStepsEmitted() =
+        testScope.runTest {
+            val steps by collectValues(underTest.transitions.dropWhile { step -> step.from == OFF })
+
+            var sentForceFinish = false
+
+            runner.startTransition(
+                this,
+                TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, getAnimator()),
+                maxFrames = 100,
+                // Force-finish on the second frame.
+                frameCallback = { frameNumber ->
+                    if (!sentForceFinish && frameNumber > 1) {
+                        testScope.launch { underTest.forceFinishCurrentTransition() }
+                        sentForceFinish = true
+                    }
+                },
+            )
+
+            val lastTwoRunningSteps =
+                steps.filter { it.transitionState == TransitionState.RUNNING }.takeLast(2)
+
+            // Make sure we stopped emitting RUNNING steps early, but then emitted a final 1f step.
+            assertTrue(lastTwoRunningSteps[0].value < 0.5f)
+            assertTrue(lastTwoRunningSteps[1].value == 1f)
+
+            assertEquals(steps.last().from, AOD)
+            assertEquals(steps.last().to, LOCKSCREEN)
+            assertEquals(steps.last().transitionState, TransitionState.FINISHED)
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_TRANSITION_FORCE_FINISH_ON_SCREEN_OFF)
+    fun forceFinishCurrentTransition_noTransitionStarted_noStepsEmitted() =
+        testScope.runTest {
+            val steps by collectValues(underTest.transitions.dropWhile { step -> step.from == OFF })
+
+            underTest.forceFinishCurrentTransition()
+            assertEquals(0, steps.size)
+        }
+
     private fun listWithStep(
         step: BigDecimal,
         start: BigDecimal = BigDecimal.ZERO,
@@ -505,7 +556,7 @@
                     to,
                     fractions[0].toFloat(),
                     TransitionState.STARTED,
-                    OWNER_NAME
+                    OWNER_NAME,
                 )
             )
         fractions.forEachIndexed { index, fraction ->
@@ -519,7 +570,7 @@
                         to,
                         fraction.toFloat(),
                         TransitionState.RUNNING,
-                        OWNER_NAME
+                        OWNER_NAME,
                     )
                 )
         }
@@ -538,6 +589,7 @@
         return ValueAnimator().apply {
             setInterpolator(Interpolators.LINEAR)
             setDuration(10)
+            addListener(animatorListener)
         }
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt
index 8914c80..ae2a5c5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt
@@ -34,6 +34,7 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import android.provider.Settings
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -43,6 +44,7 @@
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.flags.DisableSceneContainer
 import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.keyguard.KeyguardViewMediator
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
@@ -60,10 +62,13 @@
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
 import com.android.systemui.testKosmos
+import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository
 import com.google.common.truth.Truth.assertThat
 import junit.framework.Assert.assertFalse
 import junit.framework.Assert.assertTrue
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -96,7 +101,7 @@
                 from = KeyguardState.LOCKSCREEN,
                 to = KeyguardState.AOD,
                 testScope = testScope,
-                throughTransitionState = TransitionState.RUNNING
+                throughTransitionState = TransitionState.RUNNING,
             )
 
             powerInteractor.onCameraLaunchGestureDetected()
@@ -134,7 +139,7 @@
                 from = KeyguardState.AOD,
                 to = KeyguardState.LOCKSCREEN,
                 testScope = testScope,
-                throughTransitionState = TransitionState.RUNNING
+                throughTransitionState = TransitionState.RUNNING,
             )
 
             powerInteractor.onCameraLaunchGestureDetected()
@@ -182,21 +187,12 @@
             kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true)
             runCurrent()
 
-            assertThat(values)
-                .containsExactly(
-                    false,
-                    true,
-                )
+            assertThat(values).containsExactly(false, true)
 
             kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(false)
             runCurrent()
 
-            assertThat(values)
-                .containsExactly(
-                    false,
-                    true,
-                    false,
-                )
+            assertThat(values).containsExactly(false, true, false)
 
             kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true)
             runCurrent()
@@ -228,7 +224,7 @@
                 from = KeyguardState.GONE,
                 to = KeyguardState.AOD,
                 testScope = testScope,
-                throughTransitionState = TransitionState.RUNNING
+                throughTransitionState = TransitionState.RUNNING,
             )
 
             powerInteractor.onCameraLaunchGestureDetected()
@@ -242,10 +238,7 @@
                 testScope = testScope,
             )
 
-            assertThat(values)
-                .containsExactly(
-                    false,
-                )
+            assertThat(values).containsExactly(false)
         }
 
     @Test
@@ -263,7 +256,7 @@
                 from = KeyguardState.UNDEFINED,
                 to = KeyguardState.AOD,
                 testScope = testScope,
-                throughTransitionState = TransitionState.RUNNING
+                throughTransitionState = TransitionState.RUNNING,
             )
 
             powerInteractor.onCameraLaunchGestureDetected()
@@ -278,10 +271,7 @@
                 testScope = testScope,
             )
 
-            assertThat(values)
-                .containsExactly(
-                    false,
-                )
+            assertThat(values).containsExactly(false)
         }
 
     @Test
@@ -304,8 +294,19 @@
             assertThat(occludingActivityWillDismissKeyguard).isTrue()
 
             // Re-lock device:
-            kosmos.powerInteractor.setAsleepForTest()
-            runCurrent()
+            lockDevice()
             assertThat(occludingActivityWillDismissKeyguard).isFalse()
         }
+
+    private suspend fun TestScope.lockDevice() {
+        kosmos.powerInteractor.setAsleepForTest()
+        advanceTimeBy(
+            kosmos.userAwareSecureSettingsRepository
+                .getInt(
+                    Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT,
+                    KeyguardViewMediator.KEYGUARD_LOCK_AFTER_DELAY_DEFAULT,
+                )
+                .toLong()
+        )
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
index ecc62e9..87ab3c8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
@@ -69,7 +69,9 @@
         get() =
             kosmos.fakeSystemBarUtilsProxy.getStatusBarHeight() +
                 context.resources.getDimensionPixelSize(customR.dimen.small_clock_padding_top) +
-                context.resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset)
+                context.resources.getDimensionPixelSize(
+                    customR.dimen.keyguard_smartspace_top_offset
+                )
 
     private val LARGE_CLOCK_TOP
         get() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt
index 1abb441..5798e07 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt
@@ -21,6 +21,7 @@
 import android.view.Choreographer.FrameCallback
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.TransitionInfo
+import java.util.function.Consumer
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Job
@@ -35,9 +36,8 @@
  * Gives direct control over ValueAnimator, in order to make transition tests deterministic. See
  * [AnimationHandler]. Animators are required to be run on the main thread, so dispatch accordingly.
  */
-class KeyguardTransitionRunner(
-    val repository: KeyguardTransitionRepository,
-) : AnimationFrameCallbackProvider {
+class KeyguardTransitionRunner(val repository: KeyguardTransitionRepository) :
+    AnimationFrameCallbackProvider {
 
     private var frameCount = 1L
     private var frames = MutableStateFlow(Pair<Long, FrameCallback?>(0L, null))
@@ -48,7 +48,12 @@
      * For transitions being directed by an animator. Will control the number of frames being
      * generated so the values are deterministic.
      */
-    suspend fun startTransition(scope: CoroutineScope, info: TransitionInfo, maxFrames: Int = 100) {
+    suspend fun startTransition(
+        scope: CoroutineScope,
+        info: TransitionInfo,
+        maxFrames: Int = 100,
+        frameCallback: Consumer<Long>? = null,
+    ) {
         // AnimationHandler uses ThreadLocal storage, and ValueAnimators MUST start from main
         // thread
         withContext(Dispatchers.Main) {
@@ -62,7 +67,12 @@
 
                     isTerminated = frameNumber >= maxFrames
                     if (!isTerminated) {
-                        withContext(Dispatchers.Main) { callback?.doFrame(frameNumber) }
+                        try {
+                            withContext(Dispatchers.Main) { callback?.doFrame(frameNumber) }
+                            frameCallback?.accept(frameNumber)
+                        } catch (e: IllegalStateException) {
+                            e.printStackTrace()
+                        }
                     }
                 }
             }
@@ -90,9 +100,13 @@
     override fun postFrameCallback(cb: FrameCallback) {
         frames.value = Pair(frameCount++, cb)
     }
+
     override fun postCommitCallback(runnable: Runnable) {}
+
     override fun getFrameTime() = frameCount
+
     override fun getFrameDelay() = 1L
+
     override fun setFrameDelay(delay: Long) {}
 
     companion object {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryTest.kt
index 8e67e60..f8f6fe2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryTest.kt
@@ -38,6 +38,7 @@
 import com.android.systemui.media.controls.util.fakeSessionTokenFactory
 import com.android.systemui.res.R
 import com.android.systemui.testKosmos
+import com.android.systemui.util.concurrency.execution
 import com.google.common.collect.ImmutableList
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runCurrent
@@ -105,6 +106,7 @@
                 kosmos.looper,
                 handler,
                 kosmos.testScope,
+                kosmos.execution,
             )
 
         controllerFactory.setMedia3Controller(media3Controller)
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 3910903..ae7c44e 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
@@ -35,7 +35,7 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class EditTileListStateTest : SysuiTestCase() {
-    private val underTest = EditTileListState(TestEditTiles, 4)
+    private val underTest = EditTileListState(TestEditTiles, columns = 4, largeTilesSpan = 2)
 
     @Test
     fun startDrag_listHasSpacers() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java
index 0729e2f..03c1f92 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/QuickAccessWalletTileTest.java
@@ -93,6 +93,8 @@
     private static final String CARD_DESCRIPTION = "•••• 1234";
     private static final Icon CARD_IMAGE =
             Icon.createWithBitmap(Bitmap.createBitmap(70, 50, Bitmap.Config.ARGB_8888));
+    private static final Icon INVALID_CARD_IMAGE =
+            Icon.createWithContentUri("content://media/external/images/media");
     private static final int PRIMARY_USER_ID = 0;
     private static final int SECONDARY_USER_ID = 10;
 
@@ -444,6 +446,14 @@
     }
 
     @Test
+    public void testQueryCards_invalidDrawable_noSideViewDrawable() {
+        when(mKeyguardStateController.isUnlocked()).thenReturn(true);
+        setUpInvalidWalletCard(/* hasCard= */ true);
+
+        assertNull(mTile.getState().sideViewCustomDrawable);
+    }
+
+    @Test
     public void testQueryCards_error_notUpdateSideViewDrawable() {
         String errorMessage = "getWalletCardsError";
         GetWalletCardsError error = new GetWalletCardsError(CARD_IMAGE, errorMessage);
@@ -503,9 +513,33 @@
         mTestableLooper.processAllMessages();
     }
 
+    private void setUpInvalidWalletCard(boolean hasCard) {
+        GetWalletCardsResponse response =
+                new GetWalletCardsResponse(
+                        hasCard
+                                ? Collections.singletonList(createInvalidWalletCard(mContext))
+                                : Collections.EMPTY_LIST, 0);
+
+        mTile.handleSetListening(true);
+
+        verify(mController).queryWalletCards(mCallbackCaptor.capture());
+
+        mCallbackCaptor.getValue().onWalletCardsRetrieved(response);
+        mTestableLooper.processAllMessages();
+    }
+
     private WalletCard createWalletCard(Context context) {
         PendingIntent pendingIntent =
                 PendingIntent.getActivity(context, 0, mWalletIntent, PendingIntent.FLAG_IMMUTABLE);
         return new WalletCard.Builder(CARD_ID, CARD_IMAGE, CARD_DESCRIPTION, pendingIntent).build();
     }
+
+    private WalletCard createInvalidWalletCard(Context context) {
+        PendingIntent pendingIntent =
+                PendingIntent.getActivity(context, 0, mWalletIntent, PendingIntent.FLAG_IMMUTABLE);
+        return new WalletCard.Builder(
+                CARD_ID, INVALID_CARD_IMAGE, CARD_DESCRIPTION, pendingIntent).build();
+    }
+
+
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index 3be8a38..b5f005c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -18,6 +18,7 @@
 
 package com.android.systemui.scene
 
+import android.provider.Settings
 import android.telephony.TelephonyManager
 import android.testing.TestableLooper.RunWithLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -42,6 +43,7 @@
 import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.keyguard.KeyguardViewMediator
 import com.android.systemui.keyguard.ui.viewmodel.lockscreenUserActionsViewModel
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.collectLastValue
@@ -64,6 +66,7 @@
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository
 import com.android.telecom.mockTelecomManager
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
@@ -72,6 +75,7 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import org.junit.Before
 import org.junit.Test
@@ -541,7 +545,14 @@
             .isTrue()
 
         powerInteractor.setAsleepForTest()
-        testScope.runCurrent()
+        testScope.advanceTimeBy(
+            kosmos.userAwareSecureSettingsRepository
+                .getInt(
+                    Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT,
+                    KeyguardViewMediator.KEYGUARD_LOCK_AFTER_DELAY_DEFAULT,
+                )
+                .toLong()
+        )
 
         powerInteractor.setAwakeForTest()
         testScope.runCurrent()
@@ -631,14 +642,25 @@
     }
 
     /** Changes device wakefulness state from awake to asleep, going through intermediary states. */
-    private fun Kosmos.putDeviceToSleep() {
+    private suspend fun Kosmos.putDeviceToSleep(waitForLock: Boolean = true) {
         val wakefulnessModel = powerInteractor.detailedWakefulness.value
         assertWithMessage("Cannot put device to sleep as it's already asleep!")
             .that(wakefulnessModel.isAwake())
             .isTrue()
 
         powerInteractor.setAsleepForTest()
-        testScope.runCurrent()
+        if (waitForLock) {
+            testScope.advanceTimeBy(
+                kosmos.userAwareSecureSettingsRepository
+                    .getInt(
+                        Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT,
+                        KeyguardViewMediator.KEYGUARD_LOCK_AFTER_DELAY_DEFAULT,
+                    )
+                    .toLong()
+            )
+        } else {
+            testScope.runCurrent()
+        }
     }
 
     /** Emulates the dismissal of the IME (soft keyboard). */
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 08b9961..152911a 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
@@ -23,6 +23,7 @@
 import android.os.PowerManager
 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
@@ -59,6 +60,7 @@
 import com.android.systemui.haptics.msdl.fakeMSDLPlayer
 import com.android.systemui.haptics.vibratorHelper
 import com.android.systemui.keyevent.data.repository.fakeKeyEventRepository
+import com.android.systemui.keyguard.KeyguardViewMediator
 import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
 import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
@@ -106,6 +108,7 @@
 import com.android.systemui.statusbar.sysuiStatusBarStateController
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository
 import com.google.android.msdl.data.model.MSDLToken
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -1339,7 +1342,14 @@
             // Putting the device to sleep to lock it again, which shouldn't report another
             // successful unlock.
             kosmos.powerInteractor.setAsleepForTest()
-            runCurrent()
+            advanceTimeBy(
+                kosmos.userAwareSecureSettingsRepository
+                    .getInt(
+                        Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT,
+                        KeyguardViewMediator.KEYGUARD_LOCK_AFTER_DELAY_DEFAULT,
+                    )
+                    .toLong()
+            )
             // Verify that the startable changed the scene to Lockscreen because the device locked
             // following the sleep.
             assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java
index ea5c29e..3ad41a5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.notification.collection.coordinator;
 
+import static com.android.systemui.flags.SceneContainerFlagParameterizationKt.parameterizeSceneContainerFlag;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static junit.framework.Assert.assertFalse;
@@ -32,9 +34,9 @@
 import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
 
 import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.FlagsParameterization;
 import android.testing.TestableLooper;
 
-import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.compose.animation.scene.ObservableTransitionState;
@@ -42,6 +44,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.communal.shared.model.CommunalScenes;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.BrokenWithSceneContainer;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.keyguard.shared.model.KeyguardState;
 import com.android.systemui.keyguard.shared.model.TransitionState;
@@ -78,14 +81,23 @@
 import org.mockito.MockitoAnnotations;
 import org.mockito.verification.VerificationMode;
 
+import java.util.List;
+
 import kotlinx.coroutines.flow.MutableStateFlow;
 import kotlinx.coroutines.test.TestScope;
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
 
 @SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedAndroidJunit4.class)
 @TestableLooper.RunWithLooper
 public class VisualStabilityCoordinatorTest extends SysuiTestCase {
 
+    @Parameters(name = "{0}")
+    public static List<FlagsParameterization> getParams() {
+        return parameterizeSceneContainerFlag();
+    }
+
     private VisualStabilityCoordinator mCoordinator;
 
     @Mock private DumpManager mDumpManager;
@@ -117,6 +129,11 @@
     private NotificationEntry mEntry;
     private GroupEntry mGroupEntry;
 
+    public VisualStabilityCoordinatorTest(FlagsParameterization flags) {
+        super();
+        mSetFlagsRule.setFlagsParameterization(flags);
+    }
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -251,6 +268,7 @@
     }
 
     @Test
+    @BrokenWithSceneContainer(bugId = 377868472) // mReorderingAllowed is broken with SceneContainer
     public void testLockscreenPartlyShowing_groupAndSectionChangesNotAllowed() {
         // GIVEN the panel true expanded and device isn't pulsing
         setFullyDozed(false);
@@ -267,6 +285,7 @@
     }
 
     @Test
+    @BrokenWithSceneContainer(bugId = 377868472) // mReorderingAllowed is broken with SceneContainer
     public void testLockscreenFullyShowing_groupAndSectionChangesNotAllowed() {
         // GIVEN the panel true expanded and device isn't pulsing
         setFullyDozed(false);
@@ -520,6 +539,7 @@
 
     @Test
     @EnableFlags(Flags.FLAG_CHECK_LOCKSCREEN_GONE_TRANSITION)
+    @BrokenWithSceneContainer(bugId = 377868472) // mReorderingAllowed is broken with SceneContainer
     public void testNotLockscreenInGoneTransition_invalidationCalled() {
         // GIVEN visual stability is being maintained b/c animation is playing
         mKosmos.getKeyguardTransitionRepository().sendTransitionStepJava(
@@ -589,6 +609,7 @@
     }
 
     @Test
+    @BrokenWithSceneContainer(bugId = 377868472) // mReorderingAllowed is broken with SceneContainer
     public void testCommunalShowingWillNotSuppressReordering() {
         // GIVEN panel is expanded, communal is showing, and QS is collapsed
         setPulsing(false);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
index a8bcfbc..39a1c10 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
 
+import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.telephony.CellSignalStrength
 import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
@@ -735,9 +736,10 @@
         }
 
     @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
+    @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
     @Test
     // See b/346904529 for more context
-    fun satBasedIcon_doesNotInflateSignalStrength() =
+    fun satBasedIcon_doesNotInflateSignalStrength_flagOff() =
         testScope.runTest {
             val latest by collectLastValue(underTest.signalLevelIcon)
 
@@ -756,7 +758,75 @@
             assertThat(latest!!.level).isEqualTo(4)
         }
 
+    @EnableFlags(
+        com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG,
+        com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN,
+    )
+    @Test
+    // See b/346904529 for more context
+    fun satBasedIcon_doesNotInflateSignalStrength_flagOn() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.signalLevelIcon)
+
+            // GIVEN a satellite connection
+            connectionRepository.isNonTerrestrial.value = true
+            // GIVEN this carrier has set INFLATE_SIGNAL_STRENGTH
+            connectionRepository.inflateSignalStrength.value = true
+
+            connectionRepository.satelliteLevel.value = 4
+            assertThat(latest!!.level).isEqualTo(4)
+
+            connectionRepository.inflateSignalStrength.value = true
+            connectionRepository.primaryLevel.value = 4
+
+            // Icon level is unaffected
+            assertThat(latest!!.level).isEqualTo(4)
+        }
+
     @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
+    @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+    @Test
+    fun satBasedIcon_usesPrimaryLevel_flagOff() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.signalLevelIcon)
+
+            // GIVEN a satellite connection
+            connectionRepository.isNonTerrestrial.value = true
+
+            // GIVEN primary level is set
+            connectionRepository.primaryLevel.value = 4
+            connectionRepository.satelliteLevel.value = 0
+
+            // THEN icon uses the primary level because the flag is off
+            assertThat(latest!!.level).isEqualTo(4)
+        }
+
+    @EnableFlags(
+        com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG,
+        com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN,
+    )
+    @Test
+    fun satBasedIcon_usesSatelliteLevel_flagOn() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.signalLevelIcon)
+
+            // GIVEN a satellite connection
+            connectionRepository.isNonTerrestrial.value = true
+
+            // GIVEN satellite level is set
+            connectionRepository.satelliteLevel.value = 4
+            connectionRepository.primaryLevel.value = 0
+
+            // THEN icon uses the satellite level because the flag is on
+            assertThat(latest!!.level).isEqualTo(4)
+        }
+
+    /**
+     * Context (b/377518113), this test will not be needed after FLAG_CARRIER_ROAMING_NB_IOT_NTN is
+     * rolled out. The new API should report 0 automatically if not in service.
+     */
+    @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
+    @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
     @Test
     fun satBasedIcon_reportsLevelZeroWhenOutOfService() =
         testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
index 4c7cdfa..038722c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
@@ -151,7 +151,7 @@
                 iconsInteractor.isForceHidden,
                 repository,
                 context,
-                MobileIconCarrierIdOverridesFake()
+                MobileIconCarrierIdOverridesFake(),
             )
         createAndSetViewModel()
     }
@@ -359,7 +359,7 @@
             val expected =
                 Icon.Resource(
                     THREE_G.dataType,
-                    ContentDescription.Resource(THREE_G.dataContentDescription)
+                    ContentDescription.Resource(THREE_G.dataContentDescription),
                 )
             connectionsRepository.mobileIsDefault.value = true
             repository.setNetworkTypeKey(connectionsRepository.GSM_KEY)
@@ -406,7 +406,7 @@
             val expected =
                 Icon.Resource(
                     THREE_G.dataType,
-                    ContentDescription.Resource(THREE_G.dataContentDescription)
+                    ContentDescription.Resource(THREE_G.dataContentDescription),
                 )
             repository.setNetworkTypeKey(connectionsRepository.GSM_KEY)
             repository.setDataEnabled(true)
@@ -426,7 +426,7 @@
             val initial =
                 Icon.Resource(
                     THREE_G.dataType,
-                    ContentDescription.Resource(THREE_G.dataContentDescription)
+                    ContentDescription.Resource(THREE_G.dataContentDescription),
                 )
 
             repository.setNetworkTypeKey(connectionsRepository.GSM_KEY)
@@ -448,7 +448,7 @@
             val expected =
                 Icon.Resource(
                     THREE_G.dataType,
-                    ContentDescription.Resource(THREE_G.dataContentDescription)
+                    ContentDescription.Resource(THREE_G.dataContentDescription),
                 )
             repository.dataEnabled.value = true
             var latest: Icon? = null
@@ -477,7 +477,7 @@
             val expected =
                 Icon.Resource(
                     THREE_G.dataType,
-                    ContentDescription.Resource(THREE_G.dataContentDescription)
+                    ContentDescription.Resource(THREE_G.dataContentDescription),
                 )
             assertThat(latest).isEqualTo(expected)
 
@@ -499,7 +499,7 @@
             val expected =
                 Icon.Resource(
                     THREE_G.dataType,
-                    ContentDescription.Resource(THREE_G.dataContentDescription)
+                    ContentDescription.Resource(THREE_G.dataContentDescription),
                 )
             assertThat(latest).isEqualTo(expected)
 
@@ -520,7 +520,7 @@
             val expected =
                 Icon.Resource(
                     THREE_G.dataType,
-                    ContentDescription.Resource(THREE_G.dataContentDescription)
+                    ContentDescription.Resource(THREE_G.dataContentDescription),
                 )
             assertThat(latest).isEqualTo(expected)
 
@@ -542,7 +542,7 @@
             val expected =
                 Icon.Resource(
                     connectionsRepository.defaultMobileIconGroup.value.dataType,
-                    ContentDescription.Resource(G.dataContentDescription)
+                    ContentDescription.Resource(G.dataContentDescription),
                 )
 
             assertThat(latest).isEqualTo(expected)
@@ -564,7 +564,7 @@
             val expected =
                 Icon.Resource(
                     THREE_G.dataType,
-                    ContentDescription.Resource(THREE_G.dataContentDescription)
+                    ContentDescription.Resource(THREE_G.dataContentDescription),
                 )
             assertThat(latest).isEqualTo(expected)
 
@@ -621,10 +621,7 @@
                 underTest.activityInVisible.onEach { containerVisible = it }.launchIn(this)
 
             repository.dataActivityDirection.value =
-                DataActivityModel(
-                    hasActivityIn = true,
-                    hasActivityOut = true,
-                )
+                DataActivityModel(hasActivityIn = true, hasActivityOut = true)
 
             assertThat(inVisible).isFalse()
             assertThat(outVisible).isFalse()
@@ -654,10 +651,7 @@
                 underTest.activityContainerVisible.onEach { containerVisible = it }.launchIn(this)
 
             repository.dataActivityDirection.value =
-                DataActivityModel(
-                    hasActivityIn = true,
-                    hasActivityOut = false,
-                )
+                DataActivityModel(hasActivityIn = true, hasActivityOut = false)
 
             yield()
 
@@ -666,20 +660,14 @@
             assertThat(containerVisible).isTrue()
 
             repository.dataActivityDirection.value =
-                DataActivityModel(
-                    hasActivityIn = false,
-                    hasActivityOut = true,
-                )
+                DataActivityModel(hasActivityIn = false, hasActivityOut = true)
 
             assertThat(inVisible).isFalse()
             assertThat(outVisible).isTrue()
             assertThat(containerVisible).isTrue()
 
             repository.dataActivityDirection.value =
-                DataActivityModel(
-                    hasActivityIn = false,
-                    hasActivityOut = false,
-                )
+                DataActivityModel(hasActivityIn = false, hasActivityOut = false)
 
             assertThat(inVisible).isFalse()
             assertThat(outVisible).isFalse()
@@ -709,10 +697,7 @@
                 underTest.activityContainerVisible.onEach { containerVisible = it }.launchIn(this)
 
             repository.dataActivityDirection.value =
-                DataActivityModel(
-                    hasActivityIn = true,
-                    hasActivityOut = false,
-                )
+                DataActivityModel(hasActivityIn = true, hasActivityOut = false)
 
             yield()
 
@@ -721,20 +706,14 @@
             assertThat(containerVisible).isTrue()
 
             repository.dataActivityDirection.value =
-                DataActivityModel(
-                    hasActivityIn = false,
-                    hasActivityOut = true,
-                )
+                DataActivityModel(hasActivityIn = false, hasActivityOut = true)
 
             assertThat(inVisible).isFalse()
             assertThat(outVisible).isTrue()
             assertThat(containerVisible).isTrue()
 
             repository.dataActivityDirection.value =
-                DataActivityModel(
-                    hasActivityIn = false,
-                    hasActivityOut = false,
-                )
+                DataActivityModel(hasActivityIn = false, hasActivityOut = false)
 
             assertThat(inVisible).isFalse()
             assertThat(outVisible).isFalse()
@@ -824,10 +803,7 @@
             // sets the background on cellular
             repository.hasPrioritizedNetworkCapabilities.value = true
             repository.dataActivityDirection.value =
-                DataActivityModel(
-                    hasActivityIn = true,
-                    hasActivityOut = true,
-                )
+                DataActivityModel(hasActivityIn = true, hasActivityOut = true)
 
             assertThat(roaming).isFalse()
             assertThat(networkTypeIcon).isNull()
@@ -838,11 +814,13 @@
         }
 
     @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
+    @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
     @Test
-    fun nonTerrestrial_usesSatelliteIcon() =
+    fun nonTerrestrial_usesSatelliteIcon_flagOff() =
         testScope.runTest {
             repository.isNonTerrestrial.value = true
             repository.setAllLevels(0)
+            repository.satelliteLevel.value = 0
 
             val latest by
                 collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class))
@@ -853,28 +831,66 @@
 
             // 1-2 -> 1 bar
             repository.setAllLevels(1)
+            repository.satelliteLevel.value = 1
             assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
 
             repository.setAllLevels(2)
+            repository.satelliteLevel.value = 2
             assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
 
             // 3-4 -> 2 bars
             repository.setAllLevels(3)
+            repository.satelliteLevel.value = 3
             assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
 
             repository.setAllLevels(4)
+            repository.satelliteLevel.value = 4
+            assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+        }
+
+    @EnableFlags(
+        com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG,
+        com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN,
+    )
+    @Test
+    fun nonTerrestrial_usesSatelliteIcon_flagOn() =
+        testScope.runTest {
+            repository.isNonTerrestrial.value = true
+            repository.satelliteLevel.value = 0
+
+            val latest by
+                collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class))
+
+            // Level 0 -> no connection
+            assertThat(latest).isNotNull()
+            assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0)
+
+            // 1-2 -> 1 bar
+            repository.satelliteLevel.value = 1
+            assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+            repository.satelliteLevel.value = 2
+            assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+            // 3-4 -> 2 bars
+            repository.satelliteLevel.value = 3
+            assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+
+            repository.satelliteLevel.value = 4
             assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
         }
 
     @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
+    @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
     @Test
-    fun satelliteIcon_ignoresInflateSignalStrength() =
+    fun satelliteIcon_ignoresInflateSignalStrength_flagOff() =
         testScope.runTest {
             // Note that this is the exact same test as above, but with inflateSignalStrength set to
             // true we note that the level is unaffected by inflation
             repository.inflateSignalStrength.value = true
             repository.isNonTerrestrial.value = true
             repository.setAllLevels(0)
+            repository.satelliteLevel.value = 0
 
             val latest by
                 collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class))
@@ -885,16 +901,55 @@
 
             // 1-2 -> 1 bar
             repository.setAllLevels(1)
+            repository.satelliteLevel.value = 1
             assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
 
             repository.setAllLevels(2)
+            repository.satelliteLevel.value = 2
             assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
 
             // 3-4 -> 2 bars
             repository.setAllLevels(3)
+            repository.satelliteLevel.value = 3
             assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
 
             repository.setAllLevels(4)
+            repository.satelliteLevel.value = 4
+            assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+        }
+
+    @EnableFlags(
+        com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG,
+        com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN,
+    )
+    @Test
+    fun satelliteIcon_ignoresInflateSignalStrength_flagOn() =
+        testScope.runTest {
+            // Note that this is the exact same test as above, but with inflateSignalStrength set to
+            // true we note that the level is unaffected by inflation
+            repository.inflateSignalStrength.value = true
+            repository.isNonTerrestrial.value = true
+            repository.satelliteLevel.value = 0
+
+            val latest by
+                collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class))
+
+            // Level 0 -> no connection
+            assertThat(latest).isNotNull()
+            assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0)
+
+            // 1-2 -> 1 bar
+            repository.satelliteLevel.value = 1
+            assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+            repository.satelliteLevel.value = 2
+            assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+            // 3-4 -> 2 bars
+            repository.satelliteLevel.value = 3
+            assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+
+            repository.satelliteLevel.value = 4
             assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
         }
 
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
index 7d55169..89da465 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
@@ -13,11 +13,20 @@
  */
 package com.android.systemui.plugins.clocks
 
+import android.content.Context
 import android.graphics.Rect
 import android.graphics.drawable.Drawable
+import android.util.DisplayMetrics
 import android.view.View
 import androidx.constraintlayout.widget.ConstraintSet
+import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
+import androidx.constraintlayout.widget.ConstraintSet.END
+import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
+import androidx.constraintlayout.widget.ConstraintSet.START
+import androidx.constraintlayout.widget.ConstraintSet.TOP
+import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT
 import com.android.internal.annotations.Keep
+import com.android.internal.policy.SystemBarUtils
 import com.android.systemui.log.core.MessageBuffer
 import com.android.systemui.plugins.Plugin
 import com.android.systemui.plugins.annotations.GeneratedImport
@@ -149,7 +158,7 @@
 
     @ProtectedReturn("return constraints;")
     /** Custom constraints to apply to preview ConstraintLayout. */
-    fun applyPreviewConstraints(constraints: ConstraintSet): ConstraintSet
+    fun applyPreviewConstraints(context: Context, constraints: ConstraintSet): ConstraintSet
 
     fun applyAodBurnIn(aodBurnInModel: AodClockBurnInModel)
 }
@@ -169,13 +178,84 @@
         return constraints
     }
 
-    override fun applyPreviewConstraints(constraints: ConstraintSet): ConstraintSet {
-        return constraints
+    override fun applyPreviewConstraints(
+        context: Context,
+        constraints: ConstraintSet,
+    ): ConstraintSet {
+        return applyDefaultPreviewConstraints(context, constraints)
     }
 
     override fun applyAodBurnIn(aodBurnInModel: AodClockBurnInModel) {
         // Default clock doesn't need detailed control of view
     }
+
+    companion object {
+        fun applyDefaultPreviewConstraints(
+            context: Context,
+            constraints: ConstraintSet,
+        ): ConstraintSet {
+            constraints.apply {
+                val lockscreenClockViewLargeId = getId(context, "lockscreen_clock_view_large")
+                constrainWidth(lockscreenClockViewLargeId, WRAP_CONTENT)
+                constrainHeight(lockscreenClockViewLargeId, WRAP_CONTENT)
+                constrainMaxHeight(lockscreenClockViewLargeId, 0)
+
+                val largeClockTopMargin =
+                    SystemBarUtils.getStatusBarHeight(context) +
+                        getDimen(context, "small_clock_padding_top") +
+                        getDimen(context, "keyguard_smartspace_top_offset") +
+                        getDimen(context, "date_weather_view_height") +
+                        getDimen(context, "enhanced_smartspace_height")
+                connect(lockscreenClockViewLargeId, TOP, PARENT_ID, TOP, largeClockTopMargin)
+                connect(lockscreenClockViewLargeId, START, PARENT_ID, START)
+                connect(lockscreenClockViewLargeId, END, PARENT_ID, END)
+
+                // In preview, we'll show UDFPS icon for UDFPS devices
+                // and nothing for non-UDFPS devices,
+                // and we're not planning to add this vide in clockHostView
+                // so we only need position of device entry icon to constrain clock
+                // Copied calculation codes from applyConstraints in DefaultDeviceEntrySection
+                val bottomPaddingPx = getDimen(context, "lock_icon_margin_bottom")
+                val defaultDensity =
+                    DisplayMetrics.DENSITY_DEVICE_STABLE.toFloat() /
+                        DisplayMetrics.DENSITY_DEFAULT.toFloat()
+                val lockIconRadiusPx = (defaultDensity * 36).toInt()
+                val clockBottomMargin = bottomPaddingPx + 2 * lockIconRadiusPx
+
+                connect(lockscreenClockViewLargeId, BOTTOM, PARENT_ID, BOTTOM, clockBottomMargin)
+                val smallClockViewId = getId(context, "lockscreen_clock_view")
+                constrainWidth(smallClockViewId, WRAP_CONTENT)
+                constrainHeight(smallClockViewId, getDimen(context, "small_clock_height"))
+                connect(
+                    smallClockViewId,
+                    START,
+                    PARENT_ID,
+                    START,
+                    getDimen(context, "clock_padding_start") +
+                        getDimen(context, "status_view_margin_horizontal"),
+                )
+                val smallClockTopMargin =
+                    getDimen(context, "keyguard_clock_top_margin") +
+                        SystemBarUtils.getStatusBarHeight(context)
+                connect(smallClockViewId, TOP, PARENT_ID, TOP, smallClockTopMargin)
+            }
+            return constraints
+        }
+
+        fun getId(context: Context, name: String): Int {
+            val packageName = context.packageName
+            val res = context.packageManager.getResourcesForApplication(packageName)
+            val id = res.getIdentifier(name, "id", packageName)
+            return id
+        }
+
+        fun getDimen(context: Context, name: String): Int {
+            val packageName = context.packageName
+            val res = context.packageManager.getResourcesForApplication(packageName)
+            val id = res.getIdentifier(name, "dimen", packageName)
+            return if (id == 0) 0 else res.getDimensionPixelSize(id)
+        }
+    }
 }
 
 /** Events that should call when various rendering parameters change */
diff --git a/packages/SystemUI/res/values-sw600dp-land/dimens.xml b/packages/SystemUI/res/values-sw600dp-land/dimens.xml
index 2a27b47..4a53df9 100644
--- a/packages/SystemUI/res/values-sw600dp-land/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp-land/dimens.xml
@@ -24,7 +24,6 @@
     <!-- margin from keyguard status bar to clock. For split shade it should be
          keyguard_split_shade_top_margin - status_bar_header_height_keyguard = 8dp -->
     <dimen name="keyguard_clock_top_margin">8dp</dimen>
-    <dimen name="keyguard_smartspace_top_offset">0dp</dimen>
 
     <!-- QS-->
     <dimen name="qs_panel_padding_top">16dp</dimen>
diff --git a/packages/SystemUI/res/values-sw600dp-port/config.xml b/packages/SystemUI/res/values-sw600dp-port/config.xml
index f556b97..53d921b 100644
--- a/packages/SystemUI/res/values-sw600dp-port/config.xml
+++ b/packages/SystemUI/res/values-sw600dp-port/config.xml
@@ -33,6 +33,9 @@
     <!-- The number of columns in the infinite grid QuickSettings -->
     <integer name="quick_settings_infinite_grid_num_columns">6</integer>
 
+    <!-- The maximum width of large tiles in the infinite grid QuickSettings -->
+    <integer name="quick_settings_infinite_grid_tile_max_width">3</integer>
+
     <integer name="power_menu_lite_max_columns">2</integer>
     <integer name="power_menu_lite_max_rows">3</integer>
 
diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml
index 393631e..26f32ef 100644
--- a/packages/SystemUI/res/values-sw600dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp/dimens.xml
@@ -126,6 +126,4 @@
     <dimen name="controls_content_padding">24dp</dimen>
     <dimen name="control_list_vertical_spacing">8dp</dimen>
     <dimen name="control_list_horizontal_spacing">16dp</dimen>
-    <!-- For portrait direction in unfold foldable device, we don't need keyguard_smartspace_top_offset-->
-    <dimen name="keyguard_smartspace_top_offset">0dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 0854eb4..48af82a 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -79,6 +79,9 @@
     <!-- The number of columns in the infinite grid QuickSettings -->
     <integer name="quick_settings_infinite_grid_num_columns">4</integer>
 
+    <!-- The maximum width of large tiles in the infinite grid QuickSettings -->
+    <integer name="quick_settings_infinite_grid_tile_max_width">4</integer>
+
     <!-- The number of columns in the Dual Shade QuickSettings -->
     <integer name="quick_settings_dual_shade_num_columns">4</integer>
 
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 7fa2879..67eb5b0 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -815,8 +815,7 @@
     <dimen name="keyguard_clock_top_margin">18dp</dimen>
     <!-- The amount to shift the clocks during a small/large transition -->
     <dimen name="keyguard_clock_switch_y_shift">14dp</dimen>
-    <!-- When large clock is showing, offset the smartspace by this amount -->
-    <dimen name="keyguard_smartspace_top_offset">12dp</dimen>
+
     <!-- The amount to translate lockscreen elements on the GONE->AOD transition -->
     <dimen name="keyguard_enter_from_top_translation_y">-100dp</dimen>
     <!-- The amount to translate lockscreen elements on the GONE->AOD transition, on device fold -->
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 24d61911..df9f705 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -35,6 +35,7 @@
 import androidx.annotation.VisibleForTesting
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.customization.R
 import com.android.systemui.dagger.qualifiers.Background
@@ -62,6 +63,7 @@
 import com.android.systemui.plugins.clocks.ZenData
 import com.android.systemui.plugins.clocks.ZenData.ZenMode
 import com.android.systemui.res.R as SysuiR
+import com.android.systemui.settings.UserTracker
 import com.android.systemui.shared.regionsampling.RegionSampler
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback
@@ -80,7 +82,6 @@
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 /**
  * Controller for a Clock provided by the registry and used on the keyguard. Instantiated by
@@ -103,6 +104,7 @@
     private val featureFlags: FeatureFlagsClassic,
     private val zenModeController: ZenModeController,
     private val zenModeInteractor: ZenModeInteractor,
+    private val userTracker: UserTracker,
 ) {
     var loggers =
         listOf(
@@ -120,6 +122,10 @@
             connectClock(value)
         }
 
+    private fun is24HourFormat(userId: Int? = null): Boolean {
+        return DateFormat.is24HourFormat(context, userId ?: userTracker.userId)
+    }
+
     private fun disconnectClock(clock: ClockController?) {
         if (clock == null) {
             return
@@ -186,7 +192,7 @@
                 var pastVisibility: Int? = null
 
                 override fun onViewAttachedToWindow(view: View) {
-                    clock.events.onTimeFormatChanged(DateFormat.is24HourFormat(context))
+                    clock.events.onTimeFormatChanged(is24HourFormat())
                     // Match the asing for view.parent's layout classes.
                     smallClockFrame =
                         (view.parent as ViewGroup)?.also { frame ->
@@ -218,7 +224,7 @@
         largeClockOnAttachStateChangeListener =
             object : OnAttachStateChangeListener {
                 override fun onViewAttachedToWindow(p0: View) {
-                    clock.events.onTimeFormatChanged(DateFormat.is24HourFormat(context))
+                    clock.events.onTimeFormatChanged(is24HourFormat())
                 }
 
                 override fun onViewDetachedFromWindow(p0: View) {}
@@ -358,7 +364,7 @@
             }
 
             override fun onTimeFormatChanged(timeFormat: String?) {
-                clock?.run { events.onTimeFormatChanged(DateFormat.is24HourFormat(context)) }
+                clock?.run { events.onTimeFormatChanged(is24HourFormat()) }
             }
 
             override fun onTimeZoneChanged(timeZone: TimeZone) {
@@ -366,7 +372,7 @@
             }
 
             override fun onUserSwitchComplete(userId: Int) {
-                clock?.run { events.onTimeFormatChanged(DateFormat.is24HourFormat(context)) }
+                clock?.run { events.onTimeFormatChanged(is24HourFormat(userId)) }
                 zenModeCallback.onNextAlarmChanged()
             }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index 11dde6a..71d4e9a 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -147,7 +147,7 @@
         mClockSwitchYAmount = mContext.getResources().getDimensionPixelSize(
                 R.dimen.keyguard_clock_switch_y_shift);
         mSmartspaceTopOffset = (int) (mContext.getResources().getDimensionPixelSize(
-                        R.dimen.keyguard_smartspace_top_offset)
+                com.android.systemui.customization.R.dimen.keyguard_smartspace_top_offset)
                 * mContext.getResources().getConfiguration().fontScale
                 / mContext.getResources().getDisplayMetrics().density
                 * SMARTSPACE_TOP_PADDING_MULTIPLIER);
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index 811b47d..a46b236 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui;
 
+import android.animation.Animator;
 import android.annotation.SuppressLint;
 import android.app.ActivityThread;
 import android.app.Application;
@@ -135,6 +136,9 @@
         if (Flags.enableLayoutTracing()) {
             View.setTraceLayoutSteps(true);
         }
+        if (com.android.window.flags.Flags.systemUiPostAnimationEnd()) {
+            Animator.setPostNotifyEndListenerEnabled(true);
+        }
 
         if (mProcessWrapper.isSystemUser()) {
             IntentFilter bootCompletedFilter = new
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
index c9c4fd5..6635d8b 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
@@ -22,6 +22,7 @@
 import android.app.admin.DevicePolicyManager
 import android.content.IntentFilter
 import android.os.UserHandle
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.internal.widget.LockPatternUtils
 import com.android.internal.widget.LockscreenCredential
 import com.android.keyguard.KeyguardSecurityModel
@@ -57,7 +58,6 @@
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
-import com.android.app.tracing.coroutines.launchTraced as launch
 import kotlinx.coroutines.withContext
 
 /** Defines interface for classes that can access authentication-related application state. */
@@ -178,6 +178,16 @@
      * profile of an organization-owned device.
      */
     @UserIdInt suspend fun getProfileWithMinFailedUnlockAttemptsForWipe(): Int
+
+    /**
+     * Returns the device policy enforced maximum time to lock the device, in milliseconds. When the
+     * device goes to sleep, this is the maximum time the device policy allows to wait before
+     * locking the device, despite what the user setting might be set to.
+     */
+    suspend fun getMaximumTimeToLock(): Long
+
+    /** Returns `true` if the power button should instantly lock the device, `false` otherwise. */
+    suspend fun getPowerButtonInstantlyLocks(): Boolean
 }
 
 @SysUISingleton
@@ -324,6 +334,19 @@
         }
     }
 
+    override suspend fun getMaximumTimeToLock(): Long {
+        return withContext(backgroundDispatcher) {
+            devicePolicyManager.getMaximumTimeToLock(/* admin= */ null, selectedUserId)
+        }
+    }
+
+    /** Returns `true` if the power button should instantly lock the device, `false` otherwise. */
+    override suspend fun getPowerButtonInstantlyLocks(): Boolean {
+        return withContext(backgroundDispatcher) {
+            lockPatternUtils.getPowerButtonInstantlyLocks(selectedUserId)
+        }
+    }
+
     private val selectedUserId: Int
         @UserIdInt get() = userRepository.getSelectedUserInfo().id
 
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
index 67579fd..1c99473 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.authentication.domain.interactor
 
 import android.os.UserHandle
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.internal.widget.LockPatternUtils
 import com.android.internal.widget.LockPatternView
 import com.android.internal.widget.LockscreenCredential
@@ -49,7 +50,6 @@
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 /**
  * Hosts application business logic related to user authentication.
@@ -215,7 +215,7 @@
      */
     suspend fun authenticate(
         input: List<Any>,
-        tryAutoConfirm: Boolean = false
+        tryAutoConfirm: Boolean = false,
     ): AuthenticationResult {
         if (input.isEmpty()) {
             throw IllegalArgumentException("Input was empty!")
@@ -254,6 +254,20 @@
         return AuthenticationResult.FAILED
     }
 
+    /**
+     * Returns the device policy enforced maximum time to lock the device, in milliseconds. When the
+     * device goes to sleep, this is the maximum time the device policy allows to wait before
+     * locking the device, despite what the user setting might be set to.
+     */
+    suspend fun getMaximumTimeToLock(): Long {
+        return repository.getMaximumTimeToLock()
+    }
+
+    /** Returns `true` if the power button should instantly lock the device, `false` otherwise. */
+    suspend fun getPowerButtonInstantlyLocks(): Boolean {
+        return !getAuthenticationMethod().isSecure || repository.getPowerButtonInstantlyLocks()
+    }
+
     private suspend fun shouldSkipAuthenticationAttempt(
         authenticationMethod: AuthenticationMethodModel,
         isAutoConfirmAttempt: Boolean,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt
index b070068..08b3e99 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt
@@ -72,7 +72,7 @@
         // Request LockSettingsService to return the Gatekeeper Password in the
         // VerifyCredentialResponse so that we can request a Gatekeeper HAT with the
         // Gatekeeper Password and operationId.
-        var effectiveUserId = request.userInfo.userIdForPasswordEntry
+        var effectiveUserId = request.userInfo.deviceCredentialOwnerId
         val response =
             if (Flags.privateSpaceBp() && effectiveUserId != request.userInfo.userId) {
                 effectiveUserId = request.userInfo.userId
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt
index 24278ec..b74ca03 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.deviceentry.domain.interactor
 
+import android.provider.Settings
 import android.util.Log
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.CoreStartable
@@ -28,20 +29,29 @@
 import com.android.systemui.deviceentry.shared.model.DeviceUnlockSource
 import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus
 import com.android.systemui.flags.SystemPropertiesHelper
+import com.android.systemui.keyguard.KeyguardViewMediator
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.TrustInteractor
 import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.power.shared.model.WakeSleepReason
+import com.android.systemui.util.settings.repository.UserAwareSecureSettingsRepository
+import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
 import javax.inject.Inject
+import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.awaitCancellation
 import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.map
@@ -61,6 +71,8 @@
     private val powerInteractor: PowerInteractor,
     private val biometricSettingsInteractor: DeviceEntryBiometricSettingsInteractor,
     private val systemPropertiesHelper: SystemPropertiesHelper,
+    private val userAwareSecureSettingsRepository: UserAwareSecureSettingsRepository,
+    private val keyguardInteractor: KeyguardInteractor,
 ) : ExclusiveActivatable() {
 
     private val deviceUnlockSource =
@@ -176,47 +188,148 @@
                 Log.d(TAG, "remaining locked because SIM locked")
                 repository.deviceUnlockStatus.value = DeviceUnlockStatus(false, null)
             } else {
-                try {
-                    Log.d(TAG, "started watching for lock and unlock events")
-                    coroutineScope {
-                        launch {
-                            // Unlock the device when a new unlock source is detected.
-                            deviceUnlockSource.collect {
-                                Log.d(TAG, "unlocking due to \"$it\"")
-                                repository.deviceUnlockStatus.value = DeviceUnlockStatus(true, it)
-                            }
-                        }
-
-                        launch {
-                            // Lock events.
-                            merge(
-                                    // Device goes to sleep.
-                                    powerInteractor.isAsleep
-                                        .distinctUntilChanged()
-                                        .filter { it }
-                                        .map { "asleep" },
-                                    // Device enters lockdown.
-                                    isInLockdown
-                                        .distinctUntilChanged()
-                                        .filter { it }
-                                        .map { "lockdown" },
-                                )
-                                .collect { reason: String ->
-                                    Log.d(TAG, "locking due to \"$reason\"")
-                                    repository.deviceUnlockStatus.value =
-                                        DeviceUnlockStatus(false, null)
-                                }
-                        }
-                    }
-                } finally {
-                    Log.d(TAG, "stopped watching for lock and unlock events")
-                }
+                handleLockAndUnlockEvents()
             }
         }
 
         awaitCancellation()
     }
 
+    private suspend fun handleLockAndUnlockEvents() {
+        try {
+            Log.d(TAG, "started watching for lock and unlock events")
+            coroutineScope {
+                launch { handleUnlockEvents() }
+                launch { handleLockEvents() }
+            }
+        } finally {
+            Log.d(TAG, "stopped watching for lock and unlock events")
+        }
+    }
+
+    private suspend fun handleUnlockEvents() {
+        // Unlock the device when a new unlock source is detected.
+        deviceUnlockSource.collect {
+            Log.d(TAG, "unlocking due to \"$it\"")
+            repository.deviceUnlockStatus.value = DeviceUnlockStatus(true, it)
+        }
+    }
+
+    private suspend fun handleLockEvents() {
+        merge(
+                // Device wakefulness events.
+                powerInteractor.detailedWakefulness
+                    .map { Pair(it.isAsleep(), it.lastSleepReason) }
+                    .distinctUntilChangedBy { it.first }
+                    .map { (isAsleep, lastSleepReason) ->
+                        if (isAsleep) {
+                            if (
+                                lastSleepReason == WakeSleepReason.POWER_BUTTON &&
+                                    authenticationInteractor.getPowerButtonInstantlyLocks()
+                            ) {
+                                LockImmediately("locked instantly from power button")
+                            } else {
+                                LockWithDelay("entering sleep")
+                            }
+                        } else {
+                            CancelDelayedLock("waking up")
+                        }
+                    },
+                // Device enters lockdown.
+                isInLockdown
+                    .distinctUntilChanged()
+                    .filter { it }
+                    .map { LockImmediately("lockdown") },
+                // Started dreaming
+                powerInteractor.isInteractive.flatMapLatestConflated { isInteractive ->
+                    // Only respond to dream state changes while the device is interactive.
+                    if (isInteractive) {
+                        keyguardInteractor.isDreamingAny.distinctUntilChanged().map { isDreaming ->
+                            if (isDreaming) {
+                                LockWithDelay("started dreaming")
+                            } else {
+                                CancelDelayedLock("stopped dreaming")
+                            }
+                        }
+                    } else {
+                        emptyFlow()
+                    }
+                },
+            )
+            .collectLatest(::onLockEvent)
+    }
+
+    private suspend fun onLockEvent(event: LockEvent) {
+        val debugReason = event.debugReason
+        when (event) {
+            is LockImmediately -> {
+                Log.d(TAG, "locking without delay due to \"$debugReason\"")
+                repository.deviceUnlockStatus.value = DeviceUnlockStatus(false, null)
+            }
+
+            is LockWithDelay -> {
+                val lockDelay = lockDelay()
+                Log.d(TAG, "locking in ${lockDelay}ms due to \"$debugReason\"")
+                try {
+                    delay(lockDelay)
+                    Log.d(
+                        TAG,
+                        "locking after having waited for ${lockDelay}ms due to \"$debugReason\"",
+                    )
+                    repository.deviceUnlockStatus.value = DeviceUnlockStatus(false, null)
+                } catch (_: CancellationException) {
+                    Log.d(
+                        TAG,
+                        "delayed locking canceled, original delay was ${lockDelay}ms and reason was \"$debugReason\"",
+                    )
+                }
+            }
+
+            is CancelDelayedLock -> {
+                // Do nothing, the mere receipt of this inside of a "latest" block means that any
+                // previous coroutine is automatically canceled.
+            }
+        }
+    }
+
+    /**
+     * Returns the amount of time to wait before locking down the device after the device has been
+     * put to sleep by the user, in milliseconds.
+     */
+    private suspend fun lockDelay(): Long {
+        val lockAfterScreenTimeoutSetting =
+            userAwareSecureSettingsRepository
+                .getInt(
+                    Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT,
+                    KeyguardViewMediator.KEYGUARD_LOCK_AFTER_DELAY_DEFAULT,
+                )
+                .toLong()
+        Log.d(TAG, "Lock after screen timeout setting set to ${lockAfterScreenTimeoutSetting}ms")
+
+        val maxTimeToLockDevicePolicy = authenticationInteractor.getMaximumTimeToLock()
+        Log.d(TAG, "Device policy max set to ${maxTimeToLockDevicePolicy}ms")
+
+        if (maxTimeToLockDevicePolicy <= 0) {
+            // No device policy enforced maximum.
+            Log.d(TAG, "No device policy max, delay is ${lockAfterScreenTimeoutSetting}ms")
+            return lockAfterScreenTimeoutSetting
+        }
+
+        val screenOffTimeoutSetting =
+            userAwareSecureSettingsRepository
+                .getInt(
+                    Settings.System.SCREEN_OFF_TIMEOUT,
+                    KeyguardViewMediator.KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT,
+                )
+                .coerceAtLeast(0)
+                .toLong()
+        Log.d(TAG, "Screen off timeout setting set to ${screenOffTimeoutSetting}ms")
+
+        return (maxTimeToLockDevicePolicy - screenOffTimeoutSetting)
+            .coerceIn(minimumValue = 0, maximumValue = lockAfterScreenTimeoutSetting)
+            .also { Log.d(TAG, "Device policy max enforced, delay is ${it}ms") }
+    }
+
     private fun DeviceEntryRestrictionReason?.isInLockdown(): Boolean {
         return when (this) {
             DeviceEntryRestrictionReason.UserLockdown -> true
@@ -254,6 +367,16 @@
         }
     }
 
+    private sealed interface LockEvent {
+        val debugReason: String
+    }
+
+    private data class LockImmediately(override val debugReason: String) : LockEvent
+
+    private data class LockWithDelay(override val debugReason: String) : LockEvent
+
+    private data class CancelDelayedLock(override val debugReason: String) : LockEvent
+
     companion object {
         private val TAG = "DeviceUnlockedInteractor"
         @VisibleForTesting const val SYS_BOOT_REASON_PROP = "sys.boot.reason.last"
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
index dd08d32..7a95a41 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
@@ -40,7 +40,6 @@
 import com.android.internal.logging.InstanceId;
 import com.android.internal.logging.UiEvent;
 import com.android.internal.logging.UiEventLogger;
-import com.android.systemui.Flags;
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dock.DockManager;
@@ -566,8 +565,7 @@
         }
 
         // When already in pulsing, we can show the new Notification without requesting a new pulse.
-        if (Flags.notificationPulsingFix()
-                && dozeState == State.DOZE_PULSING && reason == DozeLog.PULSE_REASON_NOTIFICATION) {
+        if (dozeState == State.DOZE_PULSING && reason == DozeLog.PULSE_REASON_NOTIFICATION) {
             return;
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 60a306b..2ee9ddb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -239,7 +239,7 @@
 
     private static final boolean ENABLE_NEW_KEYGUARD_SHELL_TRANSITIONS =
             Flags.ensureKeyguardDoesTransitionStarting();
-    private static final int KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT = 30000;
+    public static final int KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT = 30000;
     private static final long KEYGUARD_DONE_PENDING_TIMEOUT_MS = 3000;
 
     private static final boolean DEBUG = KeyguardConstants.DEBUG;
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index 3a5614f..eaf8fa9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -114,6 +114,18 @@
         @FloatRange(from = 0.0, to = 1.0) value: Float,
         state: TransitionState,
     )
+
+    /**
+     * Forces the current transition to emit FINISHED, foregoing any additional RUNNING steps that
+     * otherwise would have been emitted.
+     *
+     * When the screen is off, upcoming performance changes cause all Animators to cease emitting
+     * frames, which means the Animator passed to [startTransition] will never finish if it was
+     * running when the screen turned off. Also, there's simply no reason to emit RUNNING steps when
+     * the screen isn't even on. As long as we emit FINISHED, everything should end up in the
+     * correct state.
+     */
+    suspend fun forceFinishCurrentTransition()
 }
 
 @SysUISingleton
@@ -134,6 +146,7 @@
     override val transitions = _transitions.asSharedFlow().distinctUntilChanged()
     private var lastStep: TransitionStep = TransitionStep()
     private var lastAnimator: ValueAnimator? = null
+    private var animatorListener: AnimatorListenerAdapter? = null
 
     private val withContextMutex = Mutex()
     private val _currentTransitionInfo: MutableStateFlow<TransitionInfo> =
@@ -233,7 +246,7 @@
                     )
                 }
 
-                val adapter =
+                animatorListener =
                     object : AnimatorListenerAdapter() {
                         override fun onAnimationStart(animation: Animator) {
                             emitTransition(
@@ -254,9 +267,10 @@
                             animator.removeListener(this)
                             animator.removeUpdateListener(updateListener)
                             lastAnimator = null
+                            animatorListener = null
                         }
                     }
-                animator.addListener(adapter)
+                animator.addListener(animatorListener)
                 animator.addUpdateListener(updateListener)
                 animator.start()
                 return@withContext null
@@ -290,6 +304,33 @@
         }
     }
 
+    override suspend fun forceFinishCurrentTransition() {
+        withContextMutex.lock()
+
+        if (lastAnimator?.isRunning != true) {
+            return
+        }
+
+        return withContext("$TAG#forceFinishCurrentTransition", mainDispatcher) {
+            withContextMutex.unlock()
+
+            Log.d(TAG, "forceFinishCurrentTransition() - emitting FINISHED early.")
+
+            lastAnimator?.apply {
+                // Cancel the animator, but remove listeners first so we don't emit CANCELED.
+                removeAllListeners()
+                cancel()
+
+                // Emit a final 1f RUNNING step to ensure that any transitions not listening for a
+                // FINISHED step end up in the right end state.
+                emitTransition(TransitionStep(currentTransitionInfo, 1f, TransitionState.RUNNING))
+
+                // Ask the listener to emit FINISHED and clean up its state.
+                animatorListener?.onAnimationEnd(this)
+            }
+        }
+    }
+
     private suspend fun updateTransitionInternal(
         transitionId: UUID,
         @FloatRange(from = 0.0, to = 1.0) value: Float,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index b815f19..7cd2744 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -19,8 +19,10 @@
 
 import android.annotation.SuppressLint
 import android.util.Log
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.compose.animation.scene.SceneKey
+import com.android.systemui.Flags.keyguardTransitionForceFinishOnScreenOff
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
@@ -30,6 +32,8 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState.UNDEFINED
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.power.shared.model.ScreenPowerState
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.scene.shared.model.Scenes
@@ -59,7 +63,6 @@
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.shareIn
 import kotlinx.coroutines.flow.stateIn
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 /** Encapsulates business-logic related to the keyguard transitions. */
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -70,6 +73,7 @@
     @Application val scope: CoroutineScope,
     private val repository: KeyguardTransitionRepository,
     private val sceneInteractor: SceneInteractor,
+    private val powerInteractor: PowerInteractor,
 ) {
     private val transitionMap = mutableMapOf<Edge.StateToState, MutableSharedFlow<TransitionStep>>()
 
@@ -188,6 +192,18 @@
                     }
                 }
         }
+
+        if (keyguardTransitionForceFinishOnScreenOff()) {
+            /**
+             * If the screen is turning off, finish the current transition immediately. Further
+             * frames won't be visible anyway.
+             */
+            scope.launch {
+                powerInteractor.screenPowerState
+                    .filter { it == ScreenPowerState.SCREEN_TURNING_OFF }
+                    .collect { repository.forceFinishCurrentTransition() }
+            }
+        }
     }
 
     fun transition(edge: Edge, edgeWithoutSceneContainer: Edge? = null): Flow<TransitionStep> {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
index 46f5c05..914fdd2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
@@ -18,34 +18,23 @@
 package com.android.systemui.keyguard.ui.binder
 
 import android.content.Context
-import android.util.DisplayMetrics
 import android.view.View
 import android.view.View.INVISIBLE
 import android.view.View.VISIBLE
 import android.view.ViewGroup
-import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
-import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
-import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
-import androidx.constraintlayout.widget.ConstraintSet.START
-import androidx.constraintlayout.widget.ConstraintSet.TOP
 import androidx.core.view.isVisible
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.app.tracing.coroutines.launchTraced as launch
-import com.android.internal.policy.SystemBarUtils
-import com.android.systemui.customization.R as customR
 import com.android.systemui.keyguard.shared.model.ClockSizeSetting
 import com.android.systemui.keyguard.ui.preview.KeyguardPreviewRenderer
-import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection.Companion.getDimen
 import com.android.systemui.keyguard.ui.view.layout.sections.setVisibility
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardPreviewClockViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.clocks.ClockController
-import com.android.systemui.res.R
 import com.android.systemui.shared.clocks.ClockRegistry
-import com.android.systemui.util.Utils
 import kotlin.reflect.KSuspendFunction1
 
 /** Binder for the small clock view, large clock view. */
@@ -131,78 +120,6 @@
         }
     }
 
-    private fun applyClockDefaultConstraints(context: Context, constraints: ConstraintSet) {
-        constraints.apply {
-            constrainWidth(customR.id.lockscreen_clock_view_large, ConstraintSet.WRAP_CONTENT)
-            // The following two lines make lockscreen_clock_view_large is constrained to available
-            // height when it goes beyond constraints; otherwise, it use WRAP_CONTENT
-            constrainHeight(customR.id.lockscreen_clock_view_large, WRAP_CONTENT)
-            constrainMaxHeight(customR.id.lockscreen_clock_view_large, 0)
-            val largeClockTopMargin =
-                SystemBarUtils.getStatusBarHeight(context) +
-                    context.resources.getDimensionPixelSize(customR.dimen.small_clock_padding_top) +
-                    context.resources.getDimensionPixelSize(
-                        R.dimen.keyguard_smartspace_top_offset
-                    ) +
-                    getDimen(context, DATE_WEATHER_VIEW_HEIGHT) +
-                    getDimen(context, ENHANCED_SMARTSPACE_HEIGHT)
-            connect(
-                customR.id.lockscreen_clock_view_large,
-                TOP,
-                PARENT_ID,
-                TOP,
-                largeClockTopMargin,
-            )
-            connect(customR.id.lockscreen_clock_view_large, START, PARENT_ID, START)
-            connect(
-                customR.id.lockscreen_clock_view_large,
-                ConstraintSet.END,
-                PARENT_ID,
-                ConstraintSet.END,
-            )
-
-            // In preview, we'll show UDFPS icon for UDFPS devices and nothing for non-UDFPS
-            // devices, but we need position of device entry icon to constrain clock
-            if (getConstraint(lockId) != null) {
-                connect(customR.id.lockscreen_clock_view_large, BOTTOM, lockId, TOP)
-            } else {
-                // Copied calculation codes from applyConstraints in DefaultDeviceEntrySection
-                val bottomPaddingPx =
-                    context.resources.getDimensionPixelSize(R.dimen.lock_icon_margin_bottom)
-                val defaultDensity =
-                    DisplayMetrics.DENSITY_DEVICE_STABLE.toFloat() /
-                        DisplayMetrics.DENSITY_DEFAULT.toFloat()
-                val lockIconRadiusPx = (defaultDensity * 36).toInt()
-                val clockBottomMargin = bottomPaddingPx + 2 * lockIconRadiusPx
-                connect(
-                    customR.id.lockscreen_clock_view_large,
-                    BOTTOM,
-                    PARENT_ID,
-                    BOTTOM,
-                    clockBottomMargin,
-                )
-            }
-
-            constrainWidth(customR.id.lockscreen_clock_view, WRAP_CONTENT)
-            constrainHeight(
-                customR.id.lockscreen_clock_view,
-                context.resources.getDimensionPixelSize(customR.dimen.small_clock_height),
-            )
-            connect(
-                customR.id.lockscreen_clock_view,
-                START,
-                PARENT_ID,
-                START,
-                context.resources.getDimensionPixelSize(customR.dimen.clock_padding_start) +
-                    context.resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal),
-            )
-            val smallClockTopMargin =
-                context.resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) +
-                    Utils.getStatusBarHeaderHeightKeyguard(context)
-            connect(customR.id.lockscreen_clock_view, TOP, PARENT_ID, TOP, smallClockTopMargin)
-        }
-    }
-
     private fun applyPreviewConstraints(
         context: Context,
         rootView: ConstraintLayout,
@@ -210,9 +127,8 @@
         viewModel: KeyguardPreviewClockViewModel,
     ) {
         val cs = ConstraintSet().apply { clone(rootView) }
-        applyClockDefaultConstraints(context, cs)
-        previewClock.largeClock.layout.applyPreviewConstraints(cs)
-        previewClock.smallClock.layout.applyPreviewConstraints(cs)
+        previewClock.largeClock.layout.applyPreviewConstraints(context, cs)
+        previewClock.smallClock.layout.applyPreviewConstraints(context, cs)
 
         // When selectedClockSize is the initial value, make both clocks invisible to avoid
         // flickering
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
index ee4f41d..6096cf7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
@@ -186,12 +186,23 @@
         constraints.apply {
             connect(customR.id.lockscreen_clock_view_large, START, PARENT_ID, START)
             connect(customR.id.lockscreen_clock_view_large, END, guideline, END)
-            connect(customR.id.lockscreen_clock_view_large, BOTTOM, R.id.device_entry_icon_view, TOP)
+            connect(
+                customR.id.lockscreen_clock_view_large,
+                BOTTOM,
+                R.id.device_entry_icon_view,
+                TOP,
+            )
             val largeClockTopMargin =
                 keyguardClockViewModel.getLargeClockTopMargin() +
                     getDimen(DATE_WEATHER_VIEW_HEIGHT) +
                     getDimen(ENHANCED_SMARTSPACE_HEIGHT)
-            connect(customR.id.lockscreen_clock_view_large, TOP, PARENT_ID, TOP, largeClockTopMargin)
+            connect(
+                customR.id.lockscreen_clock_view_large,
+                TOP,
+                PARENT_ID,
+                TOP,
+                largeClockTopMargin,
+            )
             constrainWidth(customR.id.lockscreen_clock_view_large, WRAP_CONTENT)
 
             // The following two lines make lockscreen_clock_view_large is constrained to available
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
index c11005d..a595d81 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
@@ -93,18 +93,18 @@
             fromBounds: Rect,
             toBounds: Rect,
             fromSSBounds: Rect?,
-            toSSBounds: Rect?
+            toSSBounds: Rect?,
         ) {}
 
         override fun createAnimator(
             sceenRoot: ViewGroup,
             startValues: TransitionValues?,
-            endValues: TransitionValues?
+            endValues: TransitionValues?,
         ): Animator? {
             if (startValues == null || endValues == null) {
                 Log.w(
                     TAG,
-                    "Couldn't create animator: startValues=$startValues; endValues=$endValues"
+                    "Couldn't create animator: startValues=$startValues; endValues=$endValues",
                 )
                 return null
             }
@@ -137,7 +137,7 @@
                         "Skipping no-op transition: $toView; " +
                             "vis: $fromVis -> $toVis; " +
                             "alpha: $fromAlpha -> $toAlpha; " +
-                            "bounds: $fromBounds -> $toBounds; "
+                            "bounds: $fromBounds -> $toBounds; ",
                     )
                 }
                 return null
@@ -151,7 +151,7 @@
                     lerp(fromBounds.left, toBounds.left, fract),
                     lerp(fromBounds.top, toBounds.top, fract),
                     lerp(fromBounds.right, toBounds.right, fract),
-                    lerp(fromBounds.bottom, toBounds.bottom, fract)
+                    lerp(fromBounds.bottom, toBounds.bottom, fract),
                 )
 
             fun assignAnimValues(src: String, fract: Float, vis: Int? = null) {
@@ -160,7 +160,7 @@
                 if (DEBUG) {
                     Log.i(
                         TAG,
-                        "$src: $toView; fract=$fract; alpha=$alpha; vis=$vis; bounds=$bounds;"
+                        "$src: $toView; fract=$fract; alpha=$alpha; vis=$vis; bounds=$bounds;",
                     )
                 }
                 toView.setVisibility(vis ?: View.VISIBLE)
@@ -174,7 +174,7 @@
                     "transitioning: $toView; " +
                         "vis: $fromVis -> $toVis; " +
                         "alpha: $fromAlpha -> $toAlpha; " +
-                        "bounds: $fromBounds -> $toBounds; "
+                        "bounds: $fromBounds -> $toBounds; ",
                 )
             }
 
@@ -258,7 +258,7 @@
             fromBounds: Rect,
             toBounds: Rect,
             fromSSBounds: Rect?,
-            toSSBounds: Rect?
+            toSSBounds: Rect?,
         ) {
             // Move normally if clock is not changing visibility
             if (fromIsVis == toIsVis) return
@@ -347,12 +347,17 @@
             fromBounds: Rect,
             toBounds: Rect,
             fromSSBounds: Rect?,
-            toSSBounds: Rect?
+            toSSBounds: Rect?,
         ) {
             // If view is changing visibility, hold it in place
             if (fromIsVis == toIsVis) return
             if (DEBUG) Log.i(TAG, "Holding position of ${view.id}")
-            toBounds.set(fromBounds)
+
+            if (fromIsVis) {
+                toBounds.set(fromBounds)
+            } else {
+                fromBounds.set(toBounds)
+            }
         }
 
         companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
index 5c79c0b..82adced 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
@@ -181,7 +181,7 @@
     fun getLargeClockTopMargin(): Int {
         return systemBarUtils.getStatusBarHeight() +
             resources.getDimensionPixelSize(customR.dimen.small_clock_padding_top) +
-            resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset)
+            resources.getDimensionPixelSize(customR.dimen.keyguard_smartspace_top_offset)
     }
 
     val largeClockTopMargin: Flow<Int> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt
index 6579ea1..65c0f57 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt
@@ -18,6 +18,7 @@
 
 import android.content.Context
 import com.android.internal.policy.SystemBarUtils
+import com.android.systemui.customization.R as customR
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.shared.model.ClockSizeSetting
 import com.android.systemui.res.R
@@ -39,20 +40,16 @@
     val selectedClockSize: StateFlow<ClockSizeSetting> = interactor.selectedClockSize
 
     val shouldHideSmartspace: Flow<Boolean> =
-        combine(
-                interactor.selectedClockSize,
-                interactor.currentClockId,
-                ::Pair,
-            )
-            .map { (size, currentClockId) ->
-                when (size) {
-                    // TODO (b/284122375) This is temporary. We should use clockController
-                    //      .largeClock.config.hasCustomWeatherDataDisplay instead, but
-                    //      ClockRegistry.createCurrentClock is not reliable.
-                    ClockSizeSetting.DYNAMIC -> currentClockId == "DIGITAL_CLOCK_WEATHER"
-                    ClockSizeSetting.SMALL -> false
-                }
+        combine(interactor.selectedClockSize, interactor.currentClockId, ::Pair).map {
+            (size, currentClockId) ->
+            when (size) {
+                // TODO (b/284122375) This is temporary. We should use clockController
+                //      .largeClock.config.hasCustomWeatherDataDisplay instead, but
+                //      ClockRegistry.createCurrentClock is not reliable.
+                ClockSizeSetting.DYNAMIC -> currentClockId == "DIGITAL_CLOCK_WEATHER"
+                ClockSizeSetting.SMALL -> false
             }
+        }
 
     fun getSmartspaceStartPadding(context: Context): Int {
         return KeyguardSmartspaceViewModel.getSmartspaceStartMargin(context)
@@ -83,7 +80,7 @@
             } else {
                 getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) +
                     SystemBarUtils.getStatusBarHeight(context) +
-                    getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset)
+                    getDimensionPixelSize(customR.dimen.keyguard_smartspace_top_offset)
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
index 850e943..ef6ae0d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
@@ -17,8 +17,10 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import android.content.res.Resources
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.biometrics.AuthController
+import com.android.systemui.customization.R as customR
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
@@ -42,7 +44,6 @@
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 class LockscreenContentViewModel
 @AssistedInject
@@ -82,10 +83,7 @@
                         unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = true),
                         unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = false),
                     ) { start, end ->
-                        UnfoldTranslations(
-                            start = start,
-                            end = end,
-                        )
+                        UnfoldTranslations(start = start, end = end)
                     }
                     .collect { _unfoldTranslations.value = it }
             }
@@ -102,17 +100,15 @@
 
     /** Returns a flow that indicates whether lockscreen notifications should be rendered. */
     fun areNotificationsVisible(): Flow<Boolean> {
-        return combine(
-            clockSize,
-            shadeInteractor.isShadeLayoutWide,
-        ) { clockSize, isShadeLayoutWide ->
+        return combine(clockSize, shadeInteractor.isShadeLayoutWide) { clockSize, isShadeLayoutWide
+            ->
             clockSize == ClockSize.SMALL || isShadeLayoutWide
         }
     }
 
     fun getSmartSpacePaddingTop(resources: Resources): Int {
         return if (clockSize.value == ClockSize.LARGE) {
-            resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset) +
+            resources.getDimensionPixelSize(customR.dimen.keyguard_smartspace_top_offset) +
                 resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin)
         } else {
             0
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt
index d38e507..913aa6f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt
@@ -42,7 +42,7 @@
 import com.android.systemui.media.controls.util.MediaControllerFactory
 import com.android.systemui.media.controls.util.SessionTokenFactory
 import com.android.systemui.res.R
-import com.android.systemui.util.Assert
+import com.android.systemui.util.concurrency.Execution
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
@@ -63,6 +63,7 @@
     @Background private val looper: Looper,
     @Background private val handler: Handler,
     @Background private val bgScope: CoroutineScope,
+    private val execution: Execution,
 ) {
 
     /**
@@ -108,7 +109,7 @@
         m3controller: Media3Controller,
         token: SessionToken,
     ): MediaButton? {
-        Assert.isNotMainThread()
+        require(!execution.isMainThread())
 
         // First, get standard actions
         val playOrPause =
diff --git a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt
index 8d48c1d..1cf4c23 100644
--- a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt
@@ -26,11 +26,13 @@
 import com.android.systemui.power.data.repository.PowerRepository
 import com.android.systemui.power.shared.model.ScreenPowerState
 import com.android.systemui.power.shared.model.WakeSleepReason
+import com.android.systemui.power.shared.model.WakefulnessModel
 import com.android.systemui.power.shared.model.WakefulnessState
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
 import javax.inject.Inject
 import javax.inject.Provider
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
 
@@ -56,7 +58,7 @@
      * Unless you need to respond differently to different [WakeSleepReason]s, you should use
      * [isAwake].
      */
-    val detailedWakefulness = repository.wakefulness
+    val detailedWakefulness: StateFlow<WakefulnessModel> = repository.wakefulness
 
     /**
      * Whether we're awake (screen is on and responding to user touch) or asleep (screen is off, or
@@ -189,9 +191,7 @@
      * In tests, you should be able to use [setAsleepForTest] rather than calling this method
      * directly.
      */
-    fun onFinishedGoingToSleep(
-        powerButtonLaunchGestureTriggeredDuringSleep: Boolean,
-    ) {
+    fun onFinishedGoingToSleep(powerButtonLaunchGestureTriggeredDuringSleep: Boolean) {
         // If the launch gesture was previously detected via onCameraLaunchGestureDetected, carry
         // that state forward. It will be reset by the next onStartedGoingToSleep.
         val powerButtonLaunchGestureTriggered =
@@ -255,7 +255,7 @@
         @JvmOverloads
         fun PowerInteractor.setAwakeForTest(
             @PowerManager.WakeReason reason: Int = PowerManager.WAKE_REASON_UNKNOWN,
-            forceEmit: Boolean = false
+            forceEmit: Boolean = false,
         ) {
             emitDuplicateWakefulnessValue = forceEmit
 
@@ -286,9 +286,7 @@
             emitDuplicateWakefulnessValue = forceEmit
 
             this.onStartedGoingToSleep(reason = sleepReason)
-            this.onFinishedGoingToSleep(
-                powerButtonLaunchGestureTriggeredDuringSleep = false,
-            )
+            this.onFinishedGoingToSleep(powerButtonLaunchGestureTriggeredDuringSleep = false)
         }
 
         /** Helper method for tests to simulate the device screen state change event. */
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/LargeTileSpanRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/LargeTileSpanRepository.kt
new file mode 100644
index 0000000..5883403
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/LargeTileSpanRepository.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.data.repository
+
+import android.content.res.Resources
+import com.android.systemui.common.ui.data.repository.ConfigurationRepository
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.res.R
+import com.android.systemui.util.kotlin.emitOnStart
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.stateIn
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class LargeTileSpanRepository
+@Inject
+constructor(
+    @Application scope: CoroutineScope,
+    @Main private val resources: Resources,
+    configurationRepository: ConfigurationRepository,
+) {
+    val span: StateFlow<Int> =
+        configurationRepository.onConfigurationChange
+            .emitOnStart()
+            .mapLatest {
+                if (resources.configuration.fontScale >= FONT_SCALE_THRESHOLD) {
+                    resources.getInteger(R.integer.quick_settings_infinite_grid_tile_max_width)
+                } else {
+                    2
+                }
+            }
+            .distinctUntilChanged()
+            .stateIn(scope, SharingStarted.WhileSubscribed(), 2)
+
+    private companion object {
+        const val FONT_SCALE_THRESHOLD = 2f
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt
index ec61a0d..23c79f5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt
@@ -21,12 +21,14 @@
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.LogLevel
 import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepository
+import com.android.systemui.qs.panels.data.repository.LargeTileSpanRepository
 import com.android.systemui.qs.panels.shared.model.PanelsLog
 import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.stateIn
 
@@ -38,6 +40,7 @@
     private val repo: DefaultLargeTilesRepository,
     private val currentTilesInteractor: CurrentTilesInteractor,
     private val preferencesInteractor: QSPreferencesInteractor,
+    largeTilesSpanRepo: LargeTileSpanRepository,
     @PanelsLog private val logBuffer: LogBuffer,
     @Application private val applicationScope: CoroutineScope,
 ) {
@@ -46,6 +49,8 @@
             .onEach { logChange(it) }
             .stateIn(applicationScope, SharingStarted.Eagerly, repo.defaultLargeTiles)
 
+    val largeTilesSpan: StateFlow<Int> = largeTilesSpanRepo.span
+
     fun isIconTile(spec: TileSpec): Boolean = !largeTilesSpecs.value.contains(spec)
 
     fun setLargeTiles(specs: Set<TileSpec>) {
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 74fa0fe..c729c7c 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
@@ -37,13 +37,17 @@
 fun rememberEditListState(
     tiles: List<SizedTile<EditTileViewModel>>,
     columns: Int,
+    largeTilesSpan: Int,
 ): EditTileListState {
-    return remember(tiles, columns) { EditTileListState(tiles, columns) }
+    return remember(tiles, columns) { EditTileListState(tiles, columns, largeTilesSpan) }
 }
 
 /** Holds the temporary state of the tile list during a drag movement where we move tiles around. */
-class EditTileListState(tiles: List<SizedTile<EditTileViewModel>>, private val columns: Int) :
-    DragAndDropState {
+class EditTileListState(
+    tiles: List<SizedTile<EditTileViewModel>>,
+    private val columns: Int,
+    private val largeTilesSpan: Int,
+) : DragAndDropState {
     private val _draggedCell = mutableStateOf<SizedTile<EditTileViewModel>?>(null)
     override val draggedCell
         get() = _draggedCell.value
@@ -86,7 +90,7 @@
         if (fromIndex != -1) {
             val cell = _tiles.removeAt(fromIndex)
             cell as TileGridCell
-            _tiles.add(fromIndex, cell.copy(width = if (cell.isIcon) 2 else 1))
+            _tiles.add(fromIndex, cell.copy(width = if (cell.isIcon) largeTilesSpan else 1))
             regenerateGrid(fromIndex)
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
index d107222..4a51bf0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt
@@ -33,7 +33,6 @@
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.text.BasicText
-import androidx.compose.material3.Icon
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
@@ -63,6 +62,7 @@
 import androidx.compose.ui.unit.dp
 import com.android.compose.modifiers.size
 import com.android.compose.modifiers.thenIf
+import com.android.compose.ui.graphics.painter.rememberDrawablePainter
 import com.android.systemui.Flags
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.ui.compose.Icon
@@ -194,26 +194,35 @@
                 is Icon.Resource -> context.getDrawable(icon.res)
             }
         }
-    if (loadedDrawable !is Animatable) {
-        Icon(icon = icon, tint = animatedColor, modifier = iconModifier)
-    } else if (icon is Icon.Resource) {
-        val image = AnimatedImageVector.animatedVectorResource(id = icon.res)
+    if (loadedDrawable is Animatable) {
         val painter =
-            key(icon) {
-                if (animateToEnd) {
-                    rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = true)
-                } else {
-                    var atEnd by remember(icon.res) { mutableStateOf(false) }
-                    LaunchedEffect(key1 = icon.res) { atEnd = true }
-                    rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd)
+            when (icon) {
+                is Icon.Resource -> {
+                    val image = AnimatedImageVector.animatedVectorResource(id = icon.res)
+                    key(icon) {
+                        if (animateToEnd) {
+                            rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = true)
+                        } else {
+                            var atEnd by remember(icon.res) { mutableStateOf(false) }
+                            LaunchedEffect(key1 = icon.res) { atEnd = true }
+                            rememberAnimatedVectorPainter(
+                                animatedImageVector = image,
+                                atEnd = atEnd,
+                            )
+                        }
+                    }
                 }
+                is Icon.Loaded -> rememberDrawablePainter(loadedDrawable)
             }
+
         Image(
             painter = painter,
             contentDescription = icon.contentDescription?.load(),
             colorFilter = ColorFilter.tint(color = animatedColor),
             modifier = iconModifier,
         )
+    } else {
+        Icon(icon = icon, tint = animatedColor, modifier = iconModifier)
     }
 }
 
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 b5cec12..31ea60e 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
@@ -26,7 +26,7 @@
 import androidx.compose.animation.fadeIn
 import androidx.compose.animation.fadeOut
 import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.LocalOverscrollConfiguration
+import androidx.compose.foundation.LocalOverscrollFactory
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
 import androidx.compose.foundation.gestures.Orientation
@@ -173,6 +173,7 @@
     listState: EditTileListState,
     otherTiles: List<SizedTile<EditTileViewModel>>,
     columns: Int,
+    largeTilesSpan: Int,
     modifier: Modifier,
     onRemoveTile: (TileSpec) -> Unit,
     onSetTiles: (List<TileSpec>) -> Unit,
@@ -203,7 +204,7 @@
         containerColor = Color.Transparent,
         topBar = { EditModeTopBar(onStopEditing = onStopEditing, onReset = reset) },
     ) { innerPadding ->
-        CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
+        CompositionLocalProvider(LocalOverscrollFactory provides null) {
             val scrollState = rememberScrollState()
             LaunchedEffect(listState.dragInProgress) {
                 if (listState.dragInProgress) {
@@ -230,7 +231,14 @@
                     }
                 }
 
-                CurrentTilesGrid(listState, selectionState, columns, onResize, onSetTiles)
+                CurrentTilesGrid(
+                    listState,
+                    selectionState,
+                    columns,
+                    largeTilesSpan,
+                    onResize,
+                    onSetTiles,
+                )
 
                 // Hide available tiles when dragging
                 AnimatedVisibility(
@@ -273,7 +281,7 @@
     ) {
         Box(
             contentAlignment = Alignment.Center,
-            modifier = modifier.fillMaxWidth().height(EditModeTileDefaults.EditGridHeaderHeight),
+            modifier = modifier.fillMaxWidth().wrapContentHeight(),
         ) {
             content()
         }
@@ -300,6 +308,7 @@
     listState: EditTileListState,
     selectionState: MutableSelectionState,
     columns: Int,
+    largeTilesSpan: Int,
     onResize: (TileSpec, toIcon: Boolean) -> Unit,
     onSetTiles: (List<TileSpec>) -> Unit,
 ) {
@@ -340,7 +349,8 @@
                 }
                 .testTag(CURRENT_TILES_GRID_TEST_TAG),
     ) {
-        EditTiles(cells, columns, listState, selectionState, coroutineScope) { spec ->
+        EditTiles(cells, columns, listState, selectionState, coroutineScope, largeTilesSpan) { spec
+            ->
             // Toggle the current size of the tile
             currentListState.isIcon(spec)?.let { onResize(spec, !it) }
         }
@@ -425,6 +435,7 @@
     dragAndDropState: DragAndDropState,
     selectionState: MutableSelectionState,
     coroutineScope: CoroutineScope,
+    largeTilesSpan: Int,
     onToggleSize: (spec: TileSpec) -> Unit,
 ) {
     items(
@@ -456,6 +467,7 @@
                         onToggleSize = onToggleSize,
                         coroutineScope = coroutineScope,
                         bounceableInfo = cells.bounceableInfo(index, columns),
+                        largeTilesSpan = largeTilesSpan,
                         modifier = Modifier.animateItem(),
                     )
                 }
@@ -472,6 +484,7 @@
     selectionState: MutableSelectionState,
     onToggleSize: (spec: TileSpec) -> Unit,
     coroutineScope: CoroutineScope,
+    largeTilesSpan: Int,
     bounceableInfo: BounceableInfo,
     modifier: Modifier = Modifier,
 ) {
@@ -514,8 +527,11 @@
                 .fillMaxWidth()
                 .onSizeChanged {
                     // Grab the size before the bounceable to get the idle width
-                    val min = if (cell.isIcon) it.width else (it.width - padding) / 2
-                    val max = if (cell.isIcon) (it.width * 2) + padding else it.width
+                    val totalPadding = (largeTilesSpan - 1) * padding
+                    val min =
+                        if (cell.isIcon) it.width else (it.width - totalPadding) / largeTilesSpan
+                    val max =
+                        if (cell.isIcon) (it.width * largeTilesSpan) + totalPadding else it.width
                     tileWidths = TileWidths(it.width, min, max)
                 }
                 .bounceable(
@@ -554,15 +570,13 @@
             val targetValue = if (cell.isIcon) 0f else 1f
             val animatedProgress = remember { Animatable(targetValue) }
 
-            if (selected) {
-                val resizingState = selectionState.resizingState
-                LaunchedEffect(targetValue, resizingState) {
-                    if (resizingState == null) {
-                        animatedProgress.animateTo(targetValue)
-                    } else {
-                        snapshotFlow { resizingState.progression }
-                            .collectLatest { animatedProgress.snapTo(it) }
-                    }
+            val resizingState = selectionState.resizingState?.takeIf { selected }
+            LaunchedEffect(targetValue, resizingState) {
+                if (resizingState == null) {
+                    animatedProgress.animateTo(targetValue)
+                } else {
+                    snapshotFlow { resizingState.progression }
+                        .collectLatest { animatedProgress.snapTo(it) }
                 }
             }
 
@@ -705,7 +719,6 @@
 
 private object EditModeTileDefaults {
     const val PLACEHOLDER_ALPHA = .3f
-    val EditGridHeaderHeight = 60.dp
     val CurrentTilesGridPadding = 8.dp
 
     @Composable
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 5ac2ad0..29ff171 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
@@ -79,7 +79,8 @@
             }
 
         val columns = columnsWithMediaViewModel.columns
-        val sizedTiles = tiles.map { SizedTileImpl(it, it.spec.width()) }
+        val largeTilesSpan by iconTilesViewModel.largeTilesSpanState
+        val sizedTiles = tiles.map { SizedTileImpl(it, it.spec.width(largeTilesSpan)) }
         val bounceables =
             remember(sizedTiles) { List(sizedTiles.size) { BounceableTileViewModel() } }
         val squishiness by viewModel.squishinessViewModel.squishiness.collectAsStateWithLifecycle()
@@ -129,21 +130,23 @@
                 viewModel.columnsWithMediaViewModelFactory.createWithoutMediaTracking()
             }
         val columns = columnsViewModel.columns
+        val largeTilesSpan by iconTilesViewModel.largeTilesSpanState
         val largeTiles by iconTilesViewModel.largeTiles.collectAsStateWithLifecycle()
 
         // Non-current tiles should always be displayed as icon tiles.
         val sizedTiles =
-            remember(tiles, largeTiles) {
+            remember(tiles, largeTiles, largeTilesSpan) {
                 tiles.map {
                     SizedTileImpl(
                         it,
-                        if (!it.isCurrent || !largeTiles.contains(it.tileSpec)) 1 else 2,
+                        if (!it.isCurrent || !largeTiles.contains(it.tileSpec)) 1
+                        else largeTilesSpan,
                     )
                 }
             }
 
         val (currentTiles, otherTiles) = sizedTiles.partition { it.tile.isCurrent }
-        val currentListState = rememberEditListState(currentTiles, columns)
+        val currentListState = rememberEditListState(currentTiles, columns, largeTilesSpan)
         DefaultEditTileGrid(
             listState = currentListState,
             otherTiles = otherTiles,
@@ -154,6 +157,7 @@
             onResize = iconTilesViewModel::resize,
             onStopEditing = onStopEditing,
             onReset = viewModel::showResetDialog,
+            largeTilesSpan = largeTilesSpan,
         )
     }
 
@@ -171,7 +175,7 @@
             .map { it.flatten().map { it.tile } }
     }
 
-    private fun TileSpec.width(): Int {
-        return if (iconTilesViewModel.isIconTile(this)) 1 else 2
+    private fun TileSpec.width(largeSize: Int = iconTilesViewModel.largeTilesSpan.value): Int {
+        return if (iconTilesViewModel.isIconTile(this)) 1 else largeSize
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
index 5bebdbc..9bbf290 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt
@@ -156,6 +156,14 @@
                     bounceEnd = currentBounceableInfo.bounceEnd,
                 ),
     ) { expandable ->
+        val longClick: (() -> Unit)? =
+            {
+                    hapticsViewModel?.setTileInteractionState(
+                        TileHapticsViewModel.TileInteractionState.LONG_CLICKED
+                    )
+                    tile.onLongClick(expandable)
+                }
+                .takeIf { uiState.handlesLongClick }
         TileContainer(
             onClick = {
                 tile.onClick(expandable)
@@ -166,12 +174,7 @@
                     coroutineScope.launch { currentBounceableInfo.bounceable.animateBounce() }
                 }
             },
-            onLongClick = {
-                hapticsViewModel?.setTileInteractionState(
-                    TileHapticsViewModel.TileInteractionState.LONG_CLICKED
-                )
-                tile.onLongClick(expandable)
-            },
+            onLongClick = longClick,
             uiState = uiState,
             iconOnly = iconOnly,
         ) {
@@ -192,14 +195,6 @@
                             tile.onSecondaryClick()
                         }
                         .takeIf { uiState.handlesSecondaryClick }
-                val longClick: (() -> Unit)? =
-                    {
-                            hapticsViewModel?.setTileInteractionState(
-                                TileHapticsViewModel.TileInteractionState.LONG_CLICKED
-                            )
-                            tile.onLongClick(expandable)
-                        }
-                        .takeIf { uiState.handlesLongClick }
                 LargeTileContent(
                     label = uiState.label,
                     secondaryLabel = uiState.secondaryLabel,
@@ -237,7 +232,7 @@
 @Composable
 fun TileContainer(
     onClick: () -> Unit,
-    onLongClick: () -> Unit,
+    onLongClick: (() -> Unit)?,
     uiState: TileUiState,
     iconOnly: Boolean,
     content: @Composable BoxScope.() -> Unit,
@@ -281,7 +276,7 @@
 @Composable
 fun Modifier.tileCombinedClickable(
     onClick: () -> Unit,
-    onLongClick: () -> Unit,
+    onLongClick: (() -> Unit)?,
     uiState: TileUiState,
     iconOnly: Boolean,
 ): Modifier {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingState.kt
index 9552aa9..41c3de5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingState.kt
@@ -22,7 +22,7 @@
 import androidx.compose.runtime.setValue
 import com.android.systemui.qs.panels.ui.compose.selection.ResizingDefaults.RESIZING_THRESHOLD
 
-class ResizingState(private val widths: TileWidths, private val onResize: () -> Unit) {
+class ResizingState(val widths: TileWidths, private val onResize: () -> Unit) {
     /** Total drag offset of this resize operation. */
     private var totalOffset by mutableFloatStateOf(0f)
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DynamicIconTilesViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DynamicIconTilesViewModel.kt
index 9feaab8..a9d673a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DynamicIconTilesViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DynamicIconTilesViewModel.kt
@@ -17,9 +17,13 @@
 package com.android.systemui.qs.panels.ui.viewmodel
 
 import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.lifecycle.Hydrator
 import com.android.systemui.qs.panels.domain.interactor.DynamicIconTilesInteractor
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
 
 /** View model to resize QS tiles down to icons when removed from the current tiles. */
 class DynamicIconTilesViewModel
@@ -28,10 +32,21 @@
     interactorFactory: DynamicIconTilesInteractor.Factory,
     iconTilesViewModel: IconTilesViewModel,
 ) : IconTilesViewModel by iconTilesViewModel, ExclusiveActivatable() {
+    private val hydrator = Hydrator("DynamicIconTilesViewModel")
     private val interactor = interactorFactory.create()
 
+    val largeTilesSpanState =
+        hydrator.hydratedStateOf(
+            traceName = "largeTilesSpan",
+            source = iconTilesViewModel.largeTilesSpan,
+        )
+
     override suspend fun onActivated(): Nothing {
-        interactor.activate()
+        coroutineScope {
+            launch { hydrator.activate() }
+            launch { interactor.activate() }
+            awaitCancellation()
+        }
     }
 
     @AssistedFactory
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt
index 4e698ed..b8c5fbb 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt
@@ -25,6 +25,8 @@
 interface IconTilesViewModel {
     val largeTiles: StateFlow<Set<TileSpec>>
 
+    val largeTilesSpan: StateFlow<Int>
+
     fun isIconTile(spec: TileSpec): Boolean
 
     fun resize(spec: TileSpec, toIcon: Boolean)
@@ -34,6 +36,7 @@
 class IconTilesViewModelImpl @Inject constructor(private val interactor: IconTilesInteractor) :
     IconTilesViewModel {
     override val largeTiles = interactor.largeTilesSpecs
+    override val largeTilesSpan = interactor.largeTilesSpan
 
     override fun isIconTile(spec: TileSpec): Boolean = interactor.isIconTile(spec)
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt
index 33ce551..adc4e4b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt
@@ -70,18 +70,21 @@
             source = quickQuickSettingsRowInteractor.rows,
         )
 
+    private val largeTilesSpan by
+        hydrator.hydratedStateOf(
+            traceName = "largeTilesSpan",
+            source = iconTilesViewModel.largeTilesSpan,
+        )
+
     private val currentTiles by
         hydrator.hydratedStateOf(traceName = "currentTiles", source = tilesInteractor.currentTiles)
 
     val tileViewModels by derivedStateOf {
         currentTiles
-            .map { SizedTileImpl(TileViewModel(it.tile, it.spec), it.spec.width) }
+            .map { SizedTileImpl(TileViewModel(it.tile, it.spec), it.spec.width()) }
             .let { splitInRowsSequence(it, columns).take(rows).toList().flatten() }
     }
 
-    private val TileSpec.width: Int
-        get() = if (largeTiles.contains(this)) 2 else 1
-
     override suspend fun onActivated(): Nothing {
         coroutineScope {
             launch { hydrator.activate() }
@@ -95,4 +98,6 @@
     interface Factory {
         fun create(): QuickQuickSettingsViewModel
     }
+
+    private fun TileSpec.width(): Int = if (largeTiles.contains(this)) largeTilesSpan else 1
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
index f218d86..37d24de 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
@@ -243,16 +243,16 @@
             }
             mSelectedCard = cards.get(selectedIndex);
             switch (mSelectedCard.getCardImage().getType()) {
-                case TYPE_URI:
-                case TYPE_URI_ADAPTIVE_BITMAP:
-                    mCardViewDrawable = null;
-                    break;
-                case TYPE_RESOURCE:
                 case TYPE_BITMAP:
                 case TYPE_ADAPTIVE_BITMAP:
-                case TYPE_DATA:
                     mCardViewDrawable = mSelectedCard.getCardImage().loadDrawable(mContext);
                     break;
+                case TYPE_URI:
+                case TYPE_URI_ADAPTIVE_BITMAP:
+                case TYPE_RESOURCE:
+                case TYPE_DATA:
+                    mCardViewDrawable = null;
+                    break;
                 default:
                     Log.e(TAG, "Unknown icon type: " + mSelectedCard.getCardImage().getType());
                     mCardViewDrawable = null;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarOrchestrator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarOrchestrator.kt
index f33b76b..ff4760f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarOrchestrator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarOrchestrator.kt
@@ -18,8 +18,10 @@
 
 import android.view.Display
 import android.view.View
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.CoreStartable
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.demomode.DemoModeController
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.DarkIconDispatcher
@@ -46,12 +48,12 @@
 import dagger.assisted.AssistedInject
 import java.io.PrintWriter
 import java.util.Optional
+import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChangedBy
 import kotlinx.coroutines.flow.filterNotNull
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 /**
  * Class responsible for managing the lifecycle and state of the status bar.
@@ -68,6 +70,7 @@
     @Assisted private val statusBarModeRepository: StatusBarModePerDisplayRepository,
     @Assisted private val statusBarInitializer: StatusBarInitializer,
     @Assisted private val statusBarWindowController: StatusBarWindowController,
+    @Main private val mainContext: CoroutineContext,
     private val demoModeController: DemoModeController,
     private val pluginDependencyProvider: PluginDependencyProvider,
     private val autoHideController: AutoHideController,
@@ -141,7 +144,8 @@
     override fun start() {
         StatusBarConnectedDisplays.assertInNewMode()
         coroutineScope
-            .launch {
+            // Perform animations on the main thread to prevent crashes.
+            .launch(context = mainContext) {
                 dumpManager.registerCriticalDumpable(dumpableName, this@StatusBarOrchestrator)
                 launch {
                     controllerAndBouncerShowing.collect { (controller, bouncerShowing) ->
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt
index 9580016..1f8d365 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt
@@ -102,6 +102,10 @@
                 return --lives <= 0
             }
         }
+
+        override fun toString(): String {
+            return "$key = $value"
+        }
     }
 
     /**
@@ -174,7 +178,10 @@
 
         pw.println("$TAG(retainCount = $retainCount, purgeTimeoutMillis = $purgeTimeoutMillis)")
         pw.withIncreasedIndent {
-            pw.printCollection("keys present in cache", cache.keys.stream().sorted().toList())
+            pw.printCollection(
+                "entries present in cache",
+                cache.values.stream().map { it.toString() }.sorted().toList(),
+            )
 
             val misses = misses.get()
             val hits = hits.get()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt
index 9cbfc44..94e9d26 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt
@@ -21,6 +21,7 @@
 import android.telephony.SignalStrength
 import android.telephony.TelephonyDisplayInfo
 import android.telephony.TelephonyManager
+import android.telephony.satellite.NtnSignalStrength
 import com.android.settingslib.SignalIcon
 import com.android.settingslib.mobile.MobileMappings
 import com.android.systemui.dagger.SysUISingleton
@@ -31,11 +32,7 @@
 
 /** Logs for inputs into the mobile pipeline. */
 @SysUISingleton
-class MobileInputLogger
-@Inject
-constructor(
-    @MobileInputLog private val buffer: LogBuffer,
-) {
+class MobileInputLogger @Inject constructor(@MobileInputLog private val buffer: LogBuffer) {
     fun logOnServiceStateChanged(serviceState: ServiceState, subId: Int) {
         buffer.log(
             TAG,
@@ -49,7 +46,7 @@
             {
                 "onServiceStateChanged: subId=$int1 emergencyOnly=$bool1 roaming=$bool2" +
                     " operator=$str1"
-            }
+            },
         )
     }
 
@@ -61,7 +58,7 @@
                 int1 = subId
                 bool1 = serviceState.isEmergencyOnly
             },
-            { "ACTION_SERVICE_STATE for subId=$int1. ServiceState.isEmergencyOnly=$bool1" }
+            { "ACTION_SERVICE_STATE for subId=$int1. ServiceState.isEmergencyOnly=$bool1" },
         )
     }
 
@@ -70,7 +67,7 @@
             TAG,
             LogLevel.INFO,
             { int1 = subId },
-            { "ACTION_SERVICE_STATE for subId=$int1. Intent is missing extras. Ignoring" }
+            { "ACTION_SERVICE_STATE for subId=$int1. Intent is missing extras. Ignoring" },
         )
     }
 
@@ -82,7 +79,16 @@
                 int1 = subId
                 str1 = signalStrength.toString()
             },
-            { "onSignalStrengthsChanged: subId=$int1 strengths=$str1" }
+            { "onSignalStrengthsChanged: subId=$int1 strengths=$str1" },
+        )
+    }
+
+    fun logNtnSignalStrengthChanged(signalStrength: NtnSignalStrength) {
+        buffer.log(
+            TAG,
+            LogLevel.INFO,
+            { int1 = signalStrength.level },
+            { "onCarrierRoamingNtnSignalStrengthChanged: level=$int1" },
         )
     }
 
@@ -128,7 +134,7 @@
             TAG,
             LogLevel.INFO,
             { bool1 = active },
-            { "onCarrierRoamingNtnModeChanged: $bool1" }
+            { "onCarrierRoamingNtnModeChanged: $bool1" },
         )
     }
 
@@ -146,12 +152,7 @@
     }
 
     fun logCarrierConfigChanged(subId: Int) {
-        buffer.log(
-            TAG,
-            LogLevel.INFO,
-            { int1 = subId },
-            { "onCarrierConfigChanged: subId=$int1" },
-        )
+        buffer.log(TAG, LogLevel.INFO, { int1 = subId }, { "onCarrierConfigChanged: subId=$int1" })
     }
 
     fun logOnDataEnabledChanged(enabled: Boolean, subId: Int) {
@@ -175,7 +176,7 @@
             TAG,
             LogLevel.INFO,
             { str1 = config.toString() },
-            { "defaultDataSubRatConfig: $str1" }
+            { "defaultDataSubRatConfig: $str1" },
         )
     }
 
@@ -184,7 +185,7 @@
             TAG,
             LogLevel.INFO,
             { str1 = mapping.toString() },
-            { "defaultMobileIconMapping: $str1" }
+            { "defaultMobileIconMapping: $str1" },
         )
     }
 
@@ -216,7 +217,7 @@
             {
                 "Intent: ACTION_SERVICE_PROVIDERS_UPDATED." +
                     " showSpn=$bool1 spn=$str1 dataSpn=$str2 showPlmn=$bool2 plmn=$str3"
-            }
+            },
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
index 205205e..07843f1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
@@ -107,6 +107,12 @@
     // @IntRange(from = 0, to = 4)
     val primaryLevel: StateFlow<Int>
 
+    /**
+     * This level can be used to reflect the signal strength when in carrier roaming NTN mode
+     * (carrier-based satellite)
+     */
+    val satelliteLevel: StateFlow<Int>
+
     /** The current data connection state. See [DataConnectionState] */
     val dataConnectionState: StateFlow<DataConnectionState>
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt
index 3261b71..be3977e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt
@@ -37,12 +37,14 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_OPERATOR
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_PRIMARY_LEVEL
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_ROAMING
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_SATELLITE_LEVEL
 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 
@@ -75,7 +77,7 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = "inflate",
-                _inflateSignalStrength.value
+                _inflateSignalStrength.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), _inflateSignalStrength.value)
 
@@ -89,7 +91,7 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_EMERGENCY,
-                _isEmergencyOnly.value
+                _isEmergencyOnly.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), _isEmergencyOnly.value)
 
@@ -100,7 +102,7 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_ROAMING,
-                _isRoaming.value
+                _isRoaming.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), _isRoaming.value)
 
@@ -111,7 +113,7 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_OPERATOR,
-                _operatorAlphaShort.value
+                _operatorAlphaShort.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), _operatorAlphaShort.value)
 
@@ -122,7 +124,7 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_IS_IN_SERVICE,
-                _isInService.value
+                _isInService.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), _isInService.value)
 
@@ -133,7 +135,7 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_IS_NTN,
-                _isNonTerrestrial.value
+                _isNonTerrestrial.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), _isNonTerrestrial.value)
 
@@ -144,7 +146,7 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_IS_GSM,
-                _isGsm.value
+                _isGsm.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), _isGsm.value)
 
@@ -155,7 +157,7 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_CDMA_LEVEL,
-                _cdmaLevel.value
+                _cdmaLevel.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), _cdmaLevel.value)
 
@@ -166,10 +168,21 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_PRIMARY_LEVEL,
-                _primaryLevel.value
+                _primaryLevel.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), _primaryLevel.value)
 
+    private val _satelliteLevel = MutableStateFlow(0)
+    override val satelliteLevel: StateFlow<Int> =
+        _satelliteLevel
+            .logDiffsForTable(
+                tableLogBuffer,
+                columnPrefix = "",
+                columnName = COL_SATELLITE_LEVEL,
+                _satelliteLevel.value,
+            )
+            .stateIn(scope, SharingStarted.WhileSubscribed(), _satelliteLevel.value)
+
     private val _dataConnectionState = MutableStateFlow(DataConnectionState.Disconnected)
     override val dataConnectionState =
         _dataConnectionState
@@ -177,12 +190,7 @@
             .stateIn(scope, SharingStarted.WhileSubscribed(), _dataConnectionState.value)
 
     private val _dataActivityDirection =
-        MutableStateFlow(
-            DataActivityModel(
-                hasActivityIn = false,
-                hasActivityOut = false,
-            )
-        )
+        MutableStateFlow(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
     override val dataActivityDirection =
         _dataActivityDirection
             .logDiffsForTable(tableLogBuffer, columnPrefix = "", _dataActivityDirection.value)
@@ -195,7 +203,7 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_CARRIER_NETWORK_CHANGE,
-                _carrierNetworkChangeActive.value
+                _carrierNetworkChangeActive.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), _carrierNetworkChangeActive.value)
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
index 2e47678..75f613d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
@@ -90,7 +90,7 @@
                         TAG,
                         "Connection repo subId=$subId " +
                             "does not equal wifi repo subId=${network.subscriptionId}; " +
-                            "not showing carrier merged"
+                            "not showing carrier merged",
                     )
                     null
                 }
@@ -149,7 +149,7 @@
             .stateIn(
                 scope,
                 SharingStarted.WhileSubscribed(),
-                ResolvedNetworkType.UnknownNetworkType
+                ResolvedNetworkType.UnknownNetworkType,
             )
 
     override val dataConnectionState =
@@ -173,6 +173,7 @@
     override val isNonTerrestrial = MutableStateFlow(false).asStateFlow()
     override val isGsm = MutableStateFlow(false).asStateFlow()
     override val carrierNetworkChangeActive = MutableStateFlow(false).asStateFlow()
+    override val satelliteLevel = MutableStateFlow(0)
 
     /**
      * Carrier merged connections happen over wifi but are displayed as a mobile triangle. Because
@@ -207,10 +208,7 @@
         @Application private val scope: CoroutineScope,
         private val wifiRepository: WifiRepository,
     ) {
-        fun build(
-            subId: Int,
-            mobileLogger: TableLogBuffer,
-        ): MobileConnectionRepository {
+        fun build(subId: Int, mobileLogger: TableLogBuffer): MobileConnectionRepository {
             return CarrierMergedConnectionRepository(
                 subId,
                 mobileLogger,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
index a5e47a6..fae9be0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
@@ -132,12 +132,12 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_EMERGENCY,
-                activeRepo.value.isEmergencyOnly.value
+                activeRepo.value.isEmergencyOnly.value,
             )
             .stateIn(
                 scope,
                 SharingStarted.WhileSubscribed(),
-                activeRepo.value.isEmergencyOnly.value
+                activeRepo.value.isEmergencyOnly.value,
             )
 
     override val isRoaming =
@@ -147,7 +147,7 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_ROAMING,
-                activeRepo.value.isRoaming.value
+                activeRepo.value.isRoaming.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.isRoaming.value)
 
@@ -158,12 +158,12 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_OPERATOR,
-                activeRepo.value.operatorAlphaShort.value
+                activeRepo.value.operatorAlphaShort.value,
             )
             .stateIn(
                 scope,
                 SharingStarted.WhileSubscribed(),
-                activeRepo.value.operatorAlphaShort.value
+                activeRepo.value.operatorAlphaShort.value,
             )
 
     override val isInService =
@@ -173,7 +173,7 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_IS_IN_SERVICE,
-                activeRepo.value.isInService.value
+                activeRepo.value.isInService.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.isInService.value)
 
@@ -184,12 +184,12 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_IS_NTN,
-                activeRepo.value.isNonTerrestrial.value
+                activeRepo.value.isNonTerrestrial.value,
             )
             .stateIn(
                 scope,
                 SharingStarted.WhileSubscribed(),
-                activeRepo.value.isNonTerrestrial.value
+                activeRepo.value.isNonTerrestrial.value,
             )
 
     override val isGsm =
@@ -199,7 +199,7 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_IS_GSM,
-                activeRepo.value.isGsm.value
+                activeRepo.value.isGsm.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.isGsm.value)
 
@@ -210,7 +210,7 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_CDMA_LEVEL,
-                activeRepo.value.cdmaLevel.value
+                activeRepo.value.cdmaLevel.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.cdmaLevel.value)
 
@@ -221,22 +221,33 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_PRIMARY_LEVEL,
-                activeRepo.value.primaryLevel.value
+                activeRepo.value.primaryLevel.value,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.primaryLevel.value)
 
+    override val satelliteLevel: StateFlow<Int> =
+        activeRepo
+            .flatMapLatest { it.satelliteLevel }
+            .logDiffsForTable(
+                tableLogBuffer,
+                columnPrefix = "",
+                columnName = COL_SATELLITE_LEVEL,
+                activeRepo.value.satelliteLevel.value,
+            )
+            .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.satelliteLevel.value)
+
     override val dataConnectionState =
         activeRepo
             .flatMapLatest { it.dataConnectionState }
             .logDiffsForTable(
                 tableLogBuffer,
                 columnPrefix = "",
-                activeRepo.value.dataConnectionState.value
+                activeRepo.value.dataConnectionState.value,
             )
             .stateIn(
                 scope,
                 SharingStarted.WhileSubscribed(),
-                activeRepo.value.dataConnectionState.value
+                activeRepo.value.dataConnectionState.value,
             )
 
     override val dataActivityDirection =
@@ -245,12 +256,12 @@
             .logDiffsForTable(
                 tableLogBuffer,
                 columnPrefix = "",
-                activeRepo.value.dataActivityDirection.value
+                activeRepo.value.dataActivityDirection.value,
             )
             .stateIn(
                 scope,
                 SharingStarted.WhileSubscribed(),
-                activeRepo.value.dataActivityDirection.value
+                activeRepo.value.dataActivityDirection.value,
             )
 
     override val carrierNetworkChangeActive =
@@ -260,12 +271,12 @@
                 tableLogBuffer,
                 columnPrefix = "",
                 columnName = COL_CARRIER_NETWORK_CHANGE,
-                activeRepo.value.carrierNetworkChangeActive.value
+                activeRepo.value.carrierNetworkChangeActive.value,
             )
             .stateIn(
                 scope,
                 SharingStarted.WhileSubscribed(),
-                activeRepo.value.carrierNetworkChangeActive.value
+                activeRepo.value.carrierNetworkChangeActive.value,
             )
 
     override val resolvedNetworkType =
@@ -274,12 +285,12 @@
             .logDiffsForTable(
                 tableLogBuffer,
                 columnPrefix = "",
-                activeRepo.value.resolvedNetworkType.value
+                activeRepo.value.resolvedNetworkType.value,
             )
             .stateIn(
                 scope,
                 SharingStarted.WhileSubscribed(),
-                activeRepo.value.resolvedNetworkType.value
+                activeRepo.value.resolvedNetworkType.value,
             )
 
     override val dataEnabled =
@@ -305,7 +316,7 @@
             .stateIn(
                 scope,
                 SharingStarted.WhileSubscribed(),
-                activeRepo.value.inflateSignalStrength.value
+                activeRepo.value.inflateSignalStrength.value,
             )
 
     override val allowNetworkSliceIndicator =
@@ -320,7 +331,7 @@
             .stateIn(
                 scope,
                 SharingStarted.WhileSubscribed(),
-                activeRepo.value.allowNetworkSliceIndicator.value
+                activeRepo.value.allowNetworkSliceIndicator.value,
             )
 
     override val numberOfLevels =
@@ -439,6 +450,7 @@
         const val COL_IS_IN_SERVICE = "isInService"
         const val COL_OPERATOR = "operatorName"
         const val COL_PRIMARY_LEVEL = "primaryLevel"
+        const val COL_SATELLITE_LEVEL = "satelliteLevel"
         const val COL_ROAMING = "roaming"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
index 62bd8ad..8a1e7f9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
@@ -41,6 +41,7 @@
 import android.telephony.TelephonyManager.EXTRA_SUBSCRIPTION_ID
 import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
 import android.telephony.TelephonyManager.UNKNOWN_CARRIER_ID
+import android.telephony.satellite.NtnSignalStrength
 import com.android.settingslib.Utils
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
@@ -173,7 +174,7 @@
 
                         override fun onDataConnectionStateChanged(
                             dataState: Int,
-                            networkType: Int
+                            networkType: Int,
                         ) {
                             logger.logOnDataConnectionStateChanged(dataState, networkType, subId)
                             trySend(CallbackEvent.OnDataConnectionStateChanged(dataState))
@@ -195,6 +196,17 @@
                             logger.logOnSignalStrengthsChanged(signalStrength, subId)
                             trySend(CallbackEvent.OnSignalStrengthChanged(signalStrength))
                         }
+
+                        override fun onCarrierRoamingNtnSignalStrengthChanged(
+                            signalStrength: NtnSignalStrength
+                        ) {
+                            logger.logNtnSignalStrengthChanged(signalStrength)
+                            trySend(
+                                CallbackEvent.OnCarrierRoamingNtnSignalStrengthChanged(
+                                    signalStrength
+                                )
+                            )
+                        }
                     }
                 telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
                 awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
@@ -267,6 +279,12 @@
             .map { it.signalStrength.level }
             .stateIn(scope, SharingStarted.WhileSubscribed(), SIGNAL_STRENGTH_NONE_OR_UNKNOWN)
 
+    override val satelliteLevel: StateFlow<Int> =
+        callbackEvents
+            .mapNotNull { it.onCarrierRoamingNtnSignalStrengthChanged }
+            .map { it.signalStrength.level }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), 0)
+
     override val dataConnectionState =
         callbackEvents
             .mapNotNull { it.onDataConnectionStateChanged }
@@ -280,7 +298,7 @@
             .stateIn(
                 scope,
                 SharingStarted.WhileSubscribed(),
-                DataActivityModel(hasActivityIn = false, hasActivityOut = false)
+                DataActivityModel(hasActivityIn = false, hasActivityOut = false),
             )
 
     override val carrierNetworkChangeActive =
@@ -385,7 +403,7 @@
                             if (
                                 intent.getIntExtra(
                                     EXTRA_SUBSCRIPTION_INDEX,
-                                    INVALID_SUBSCRIPTION_ID
+                                    INVALID_SUBSCRIPTION_ID,
                                 ) == subId
                             ) {
                                 logger.logServiceProvidersUpdatedBroadcast(intent)
@@ -399,7 +417,7 @@
 
                 context.registerReceiver(
                     receiver,
-                    IntentFilter(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED)
+                    IntentFilter(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED),
                 )
 
                 awaitClose { context.unregisterReceiver(receiver) }
@@ -524,6 +542,9 @@
     data class OnServiceStateChanged(val serviceState: ServiceState) : CallbackEvent
 
     data class OnSignalStrengthChanged(val signalStrength: SignalStrength) : CallbackEvent
+
+    data class OnCarrierRoamingNtnSignalStrengthChanged(val signalStrength: NtnSignalStrength) :
+        CallbackEvent
 }
 
 /**
@@ -539,6 +560,9 @@
     val onDisplayInfoChanged: CallbackEvent.OnDisplayInfoChanged? = null,
     val onServiceStateChanged: CallbackEvent.OnServiceStateChanged? = null,
     val onSignalStrengthChanged: CallbackEvent.OnSignalStrengthChanged? = null,
+    val onCarrierRoamingNtnSignalStrengthChanged:
+        CallbackEvent.OnCarrierRoamingNtnSignalStrengthChanged? =
+        null,
 ) {
     fun applyEvent(event: CallbackEvent): TelephonyCallbackState {
         return when (event) {
@@ -555,6 +579,8 @@
                 copy(onServiceStateChanged = event)
             }
             is CallbackEvent.OnSignalStrengthChanged -> copy(onSignalStrengthChanged = event)
+            is CallbackEvent.OnCarrierRoamingNtnSignalStrengthChanged ->
+                copy(onCarrierRoamingNtnSignalStrengthChanged = event)
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
index 4ef328c..1bf14af 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
@@ -335,7 +335,11 @@
     // Satellite level is unaffected by the inflateSignalStrength property
     // See b/346904529 for details
     private val satelliteShownLevel: StateFlow<Int> =
-        combine(level, isInService) { level, isInService -> if (isInService) level else 0 }
+        if (Flags.carrierRoamingNbIotNtn()) {
+                connectionRepository.satelliteLevel
+            } else {
+                combine(level, isInService) { level, isInService -> if (isInService) level else 0 }
+            }
             .stateIn(scope, SharingStarted.WhileSubscribed(), 0)
 
     private val cellularIcon: Flow<SignalIconModel.Cellular> =
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletScreenController.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletScreenController.java
index 53e6b4f..761993b 100644
--- a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletScreenController.java
+++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletScreenController.java
@@ -330,13 +330,19 @@
         QAWalletCardViewInfo(Context context, WalletCard walletCard) {
             mWalletCard = walletCard;
             Icon cardImageIcon = mWalletCard.getCardImage();
-            if (cardImageIcon.getType() == Icon.TYPE_URI) {
-                mCardDrawable = null;
-            } else {
+            if (cardImageIcon.getType() == Icon.TYPE_BITMAP
+                    || cardImageIcon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) {
                 mCardDrawable = mWalletCard.getCardImage().loadDrawable(context);
+            } else {
+                mCardDrawable = null;
             }
             Icon icon = mWalletCard.getCardIcon();
-            mIconDrawable = icon == null ? null : icon.loadDrawable(context);
+            if (icon != null && (icon.getType() == Icon.TYPE_BITMAP
+                    || icon.getType() == Icon.TYPE_ADAPTIVE_BITMAP)) {
+                mIconDrawable = icon.loadDrawable(context);
+            } else {
+                mIconDrawable = null;
+            }
         }
 
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java
index 1a39934..ca9b866 100644
--- a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java
+++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java
@@ -283,6 +283,11 @@
         return mCardLabel;
     }
 
+    @VisibleForTesting
+    ImageView getIcon() {
+        return mIcon;
+    }
+
     @Nullable
     private static Drawable getHeaderIcon(Context context, WalletCardViewInfo walletCard) {
         Drawable icon = walletCard.getIcon();
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index 2b167e4..65b6273 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -56,6 +56,7 @@
 import com.android.systemui.plugins.clocks.ZenData
 import com.android.systemui.plugins.clocks.ZenData.ZenMode
 import com.android.systemui.res.R
+import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.ZenModeController
@@ -128,6 +129,7 @@
     @Mock private lateinit var largeClockEvents: ClockFaceEvents
     @Mock private lateinit var parentView: View
     @Mock private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
+    @Mock private lateinit var userTracker: UserTracker
 
     @Mock private lateinit var zenModeController: ZenModeController
     private var zenModeControllerCallback: ZenModeController.Callback? = null
@@ -153,6 +155,7 @@
             .thenReturn(ClockFaceConfig(tickRate = ClockTickRate.PER_MINUTE))
         whenever(smallClockController.theme).thenReturn(ThemeConfig(true, null))
         whenever(largeClockController.theme).thenReturn(ThemeConfig(true, null))
+        whenever(userTracker.userId).thenReturn(1)
 
         zenModeRepository.addMode(MANUAL_DND_INACTIVE)
 
@@ -177,6 +180,7 @@
                 withDeps.featureFlags,
                 zenModeController,
                 kosmos.zenModeInteractor,
+                userTracker,
             )
         underTest.clock = clock
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
index 96f4a60..b4c6952 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
@@ -46,7 +46,6 @@
 
 import com.android.internal.logging.InstanceId;
 import com.android.internal.logging.UiEventLogger;
-import com.android.systemui.Flags;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.broadcast.BroadcastDispatcher;
@@ -222,7 +221,6 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_NOTIFICATION_PULSING_FIX)
     public void testOnNotification_alreadyPulsing_notificationNotSuppressed() {
         // GIVEN device is pulsing
         Runnable pulseSuppressListener = mock(Runnable.class);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
index fb376ce..3ddd4b5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
@@ -289,6 +289,7 @@
             }
         verify(smartspaceManager).createSmartspaceSession(capture(smartSpaceConfigBuilderCaptor))
         mediaControllerFactory.setControllerForToken(session.sessionToken, controller)
+        whenever(controller.sessionToken).thenReturn(session.sessionToken)
         whenever(controller.transportControls).thenReturn(transportControls)
         whenever(controller.playbackInfo).thenReturn(playbackInfo)
         whenever(controller.metadata).thenReturn(metadataBuilder.build())
@@ -1599,6 +1600,7 @@
         verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any())
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testTooManyCompactActions_isTruncated() {
         // GIVEN a notification where too many compact actions were specified
@@ -1635,6 +1637,7 @@
             .isEqualTo(LegacyMediaDataManagerImpl.MAX_COMPACT_ACTIONS)
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testTooManyNotificationActions_isTruncated() {
         // GIVEN a notification where too many notification actions are added
@@ -1670,6 +1673,7 @@
             .isEqualTo(LegacyMediaDataManagerImpl.MAX_NOTIFICATION_ACTIONS)
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackActions_noState_usesNotification() {
         val desc = "Notification Action"
@@ -1703,6 +1707,7 @@
         assertThat(mediaDataCaptor.value!!.actions[0]!!.contentDescription).isEqualTo(desc)
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackActions_hasPrevNext() {
         val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4")
@@ -1746,6 +1751,7 @@
         assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[1])
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackActions_noPrevNext_usesCustom() {
         val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4", "custom 5")
@@ -1778,6 +1784,7 @@
         assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[3])
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackActions_connecting() {
         val stateActions = PlaybackState.ACTION_PLAY
@@ -1797,6 +1804,7 @@
             .isEqualTo(context.getString(R.string.controls_media_button_connecting))
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackActions_reservedSpace() {
         val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4")
@@ -1835,6 +1843,7 @@
         assertThat(actions.reservePrev).isTrue()
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackActions_playPause_hasButton() {
         val stateActions = PlaybackState.ACTION_PLAY_PAUSE
@@ -1998,6 +2007,7 @@
             assertThat(mediaDataCaptor.value.semanticActions).isNotNull()
         }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackStateNull_Pause_keyExists_callsListener() {
         whenever(controller.playbackState).thenReturn(null)
@@ -2056,6 +2066,7 @@
         assertThat(mediaDataCaptor.value.isClearable).isFalse()
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testRetain_notifPlayer_notifRemoved_setToResume() {
         fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true)
@@ -2086,6 +2097,7 @@
             )
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testRetain_notifPlayer_sessionDestroyed_doesNotChange() {
         fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true)
@@ -2104,6 +2116,7 @@
             .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testRetain_notifPlayer_removeWhileActive_fullyRemoved() {
         fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
index 7d364bd..e5483c0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
@@ -103,7 +103,6 @@
 import org.mockito.Captor
 import org.mockito.Mock
 import org.mockito.Mockito
-import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
@@ -113,6 +112,7 @@
 import org.mockito.kotlin.any
 import org.mockito.kotlin.capture
 import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
 import org.mockito.quality.Strictness
 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
@@ -312,6 +312,7 @@
             }
         verify(smartspaceManager).createSmartspaceSession(capture(smartSpaceConfigBuilderCaptor))
         mediaControllerFactory.setControllerForToken(session.sessionToken, controller)
+        whenever(controller.sessionToken).thenReturn(session.sessionToken)
         whenever(controller.transportControls).thenReturn(transportControls)
         whenever(controller.playbackInfo).thenReturn(playbackInfo)
         whenever(controller.metadata).thenReturn(metadataBuilder.build())
@@ -596,7 +597,7 @@
     fun testOnNotificationAdded_emptyTitle_hasPlaceholder() {
         // When the manager has a notification with an empty title, and the app is not
         // required to include a non-empty title
-        val mockPackageManager = mock(PackageManager::class.java)
+        val mockPackageManager = mock<PackageManager>()
         context.setMockPackageManager(mockPackageManager)
         whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME)
         whenever(controller.metadata)
@@ -626,7 +627,7 @@
     fun testOnNotificationAdded_blankTitle_hasPlaceholder() {
         // GIVEN that the manager has a notification with a blank title, and the app is not
         // required to include a non-empty title
-        val mockPackageManager = mock(PackageManager::class.java)
+        val mockPackageManager = mock<PackageManager>()
         context.setMockPackageManager(mockPackageManager)
         whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME)
         whenever(controller.metadata)
@@ -656,7 +657,7 @@
     fun testOnNotificationAdded_emptyMetadata_usesNotificationTitle() {
         // When the app sets the metadata title fields to empty strings, but does include a
         // non-blank notification title
-        val mockPackageManager = mock(PackageManager::class.java)
+        val mockPackageManager = mock<PackageManager>()
         context.setMockPackageManager(mockPackageManager)
         whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME)
         whenever(controller.metadata)
@@ -1610,6 +1611,7 @@
         verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any())
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testTooManyCompactActions_isTruncated() {
         // GIVEN a notification where too many compact actions were specified
@@ -1646,6 +1648,7 @@
             .isEqualTo(MediaDataProcessor.MAX_COMPACT_ACTIONS)
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testTooManyNotificationActions_isTruncated() {
         // GIVEN a notification where too many notification actions are added
@@ -1681,6 +1684,7 @@
             .isEqualTo(MediaDataProcessor.MAX_NOTIFICATION_ACTIONS)
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackActions_noState_usesNotification() {
         val desc = "Notification Action"
@@ -1714,6 +1718,7 @@
         assertThat(mediaDataCaptor.value!!.actions[0]!!.contentDescription).isEqualTo(desc)
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackActions_hasPrevNext() {
         val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4")
@@ -1757,6 +1762,7 @@
         assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[1])
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackActions_noPrevNext_usesCustom() {
         val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4", "custom 5")
@@ -1789,6 +1795,7 @@
         assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[3])
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackActions_connecting() {
         val stateActions = PlaybackState.ACTION_PLAY
@@ -1874,6 +1881,7 @@
             .isNotEqualTo(firstSemanticActions.prevOrCustom?.icon)
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackActions_reservedSpace() {
         val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4")
@@ -1912,6 +1920,7 @@
         assertThat(actions.reservePrev).isTrue()
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackActions_playPause_hasButton() {
         val stateActions = PlaybackState.ACTION_PLAY_PAUSE
@@ -2074,6 +2083,7 @@
         assertThat(mediaDataCaptor.value.semanticActions).isNotNull()
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testPlaybackStateNull_Pause_keyExists_callsListener() {
         whenever(controller.playbackState).thenReturn(null)
@@ -2132,6 +2142,7 @@
         assertThat(mediaDataCaptor.value.isClearable).isFalse()
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testRetain_notifPlayer_notifRemoved_setToResume() {
         fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true)
@@ -2162,6 +2173,7 @@
             )
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testRetain_notifPlayer_sessionDestroyed_doesNotChange() {
         fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true)
@@ -2180,6 +2192,7 @@
             .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean())
     }
 
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_BUTTON_MEDIA3)
     @Test
     fun testRetain_notifPlayer_removeWhileActive_fullyRemoved() {
         fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true)
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 8a6df1c..d88d69d 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
@@ -63,6 +63,7 @@
             listState = listState,
             otherTiles = listOf(),
             columns = 4,
+            largeTilesSpan = 4,
             modifier = Modifier.fillMaxSize(),
             onRemoveTile = {},
             onSetTiles = onSetTiles,
@@ -75,7 +76,7 @@
     @Test
     fun draggedTile_shouldDisappear() {
         var tiles by mutableStateOf(TestEditTiles)
-        val listState = EditTileListState(tiles, 4)
+        val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2)
         composeRule.setContent {
             EditTileGridUnderTest(listState) {
                 tiles = it.map { tileSpec -> createEditTile(tileSpec.spec) }
@@ -101,7 +102,7 @@
     @Test
     fun draggedTile_shouldChangePosition() {
         var tiles by mutableStateOf(TestEditTiles)
-        val listState = EditTileListState(tiles, 4)
+        val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2)
         composeRule.setContent {
             EditTileGridUnderTest(listState) {
                 tiles = it.map { tileSpec -> createEditTile(tileSpec.spec) }
@@ -128,7 +129,7 @@
     @Test
     fun draggedTileOut_shouldBeRemoved() {
         var tiles by mutableStateOf(TestEditTiles)
-        val listState = EditTileListState(tiles, 4)
+        val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2)
         composeRule.setContent {
             EditTileGridUnderTest(listState) {
                 tiles = it.map { tileSpec -> createEditTile(tileSpec.spec) }
@@ -153,7 +154,7 @@
     @Test
     fun draggedNewTileIn_shouldBeAdded() {
         var tiles by mutableStateOf(TestEditTiles)
-        val listState = EditTileListState(tiles, 4)
+        val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2)
         composeRule.setContent {
             EditTileGridUnderTest(listState) {
                 tiles = it.map { tileSpec -> createEditTile(tileSpec.spec) }
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 d9c1d99..fac5ecb 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
@@ -62,6 +62,7 @@
             listState = listState,
             otherTiles = listOf(),
             columns = 4,
+            largeTilesSpan = 4,
             modifier = Modifier.fillMaxSize(),
             onRemoveTile = {},
             onSetTiles = {},
@@ -74,7 +75,7 @@
     @Test
     fun toggleIconTile_shouldBeLarge() {
         var tiles by mutableStateOf(TestEditTiles)
-        val listState = EditTileListState(tiles, 4)
+        val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2)
         composeRule.setContent {
             EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) }
         }
@@ -90,7 +91,7 @@
     @Test
     fun toggleLargeTile_shouldBeIcon() {
         var tiles by mutableStateOf(TestEditTiles)
-        val listState = EditTileListState(tiles, 4)
+        val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2)
         composeRule.setContent {
             EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) }
         }
@@ -106,7 +107,7 @@
     @Test
     fun resizedLarge_shouldBeIcon() {
         var tiles by mutableStateOf(TestEditTiles)
-        val listState = EditTileListState(tiles, 4)
+        val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2)
         composeRule.setContent {
             EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) }
         }
@@ -126,7 +127,7 @@
     @Test
     fun resizedIcon_shouldBeLarge() {
         var tiles by mutableStateOf(TestEditTiles)
-        val listState = EditTileListState(tiles, 4)
+        val listState = EditTileListState(tiles, columns = 4, largeTilesSpan = 2)
         composeRule.setContent {
             EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) }
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallet/ui/WalletScreenControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallet/ui/WalletScreenControllerTest.java
index 38a61fe..21adeb0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wallet/ui/WalletScreenControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wallet/ui/WalletScreenControllerTest.java
@@ -316,6 +316,31 @@
     }
 
     @Test
+    public void queryCards_hasCards_showCarousel_invalidIconSource_noIcon() {
+        GetWalletCardsResponse response =
+                new GetWalletCardsResponse(
+                        Collections.singletonList(createWalletCardWithInvalidIcon(mContext)), 0);
+
+        mController.queryWalletCards();
+        mTestableLooper.processAllMessages();
+
+        verify(mWalletClient).getWalletCards(any(), any(), mCallbackCaptor.capture());
+
+        QuickAccessWalletClient.OnWalletCardsRetrievedCallback callback =
+                mCallbackCaptor.getValue();
+
+        assertEquals(mController, callback);
+
+        callback.onWalletCardsRetrieved(response);
+        mTestableLooper.processAllMessages();
+
+        assertEquals(VISIBLE, mWalletView.getCardCarousel().getVisibility());
+        assertEquals(GONE, mWalletView.getEmptyStateView().getVisibility());
+        assertEquals(GONE, mWalletView.getErrorView().getVisibility());
+        assertEquals(null, mWalletView.getIcon().getDrawable());
+    }
+
+    @Test
     public void queryCards_noCards_showEmptyState() {
         GetWalletCardsResponse response = new GetWalletCardsResponse(Collections.EMPTY_LIST, 0);
 
@@ -507,6 +532,16 @@
                 .build();
     }
 
+    private WalletCard createWalletCardWithInvalidIcon(Context context) {
+        PendingIntent pendingIntent =
+                PendingIntent.getActivity(context, 0, mWalletIntent, PendingIntent.FLAG_IMMUTABLE);
+        return new WalletCard.Builder(
+                CARD_ID_1, createIconWithInvalidSource(), "•••• 1234", pendingIntent)
+                .setCardIcon(createIconWithInvalidSource())
+                .setCardLabel("Hold to reader")
+                .build();
+    }
+
     private WalletCard createCrazyWalletCard(Context context, boolean hasLabel) {
         PendingIntent pendingIntent =
                 PendingIntent.getActivity(context, 0, mWalletIntent, PendingIntent.FLAG_IMMUTABLE);
@@ -520,6 +555,10 @@
         return Icon.createWithBitmap(Bitmap.createBitmap(70, 44, Bitmap.Config.ARGB_8888));
     }
 
+    private static Icon createIconWithInvalidSource() {
+        return Icon.createWithContentUri("content://media/external/images/media");
+    }
+
     private WalletCardViewInfo createCardViewInfo(WalletCard walletCard) {
         return new WalletScreenController.QAWalletCardViewInfo(
                 mContext, walletCard);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 48106de..fc318d5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -2395,7 +2395,8 @@
 
         FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
         mBubbleController.registerBubbleStateListener(bubbleStateListener);
-        mBubbleController.setBubbleBarLocation(BubbleBarLocation.LEFT);
+        mBubbleController.setBubbleBarLocation(BubbleBarLocation.LEFT,
+                BubbleBarLocation.UpdateSource.DRAG_EXP_VIEW);
         assertThat(bubbleStateListener.mLastUpdate).isNotNull();
         assertThat(bubbleStateListener.mLastUpdate.bubbleBarLocation).isEqualTo(
                 BubbleBarLocation.LEFT);
@@ -2408,7 +2409,8 @@
 
         FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
         mBubbleController.registerBubbleStateListener(bubbleStateListener);
-        mBubbleController.setBubbleBarLocation(BubbleBarLocation.LEFT);
+        mBubbleController.setBubbleBarLocation(BubbleBarLocation.LEFT,
+                BubbleBarLocation.UpdateSource.DRAG_EXP_VIEW);
         assertThat(bubbleStateListener.mStateChangeCalls).isEqualTo(0);
     }
 
@@ -2535,6 +2537,78 @@
 
     @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
     @Test
+    public void testEventLogging_bubbleBar_dragBarLeft() {
+        mBubbleProperties.mIsBubbleBarEnabled = true;
+        mPositioner.setIsLargeScreen(true);
+        mPositioner.setBubbleBarLocation(BubbleBarLocation.RIGHT);
+        FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
+        mBubbleController.registerBubbleStateListener(bubbleStateListener);
+
+        mEntryListener.onEntryAdded(mRow);
+        assertBarMode();
+
+        mBubbleController.setBubbleBarLocation(BubbleBarLocation.LEFT,
+                BubbleBarLocation.UpdateSource.DRAG_BAR);
+
+        verify(mBubbleLogger).log(BubbleLogger.Event.BUBBLE_BAR_MOVED_LEFT_DRAG_BAR);
+    }
+
+    @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
+    @Test
+    public void testEventLogging_bubbleBar_dragBarRight() {
+        mBubbleProperties.mIsBubbleBarEnabled = true;
+        mPositioner.setIsLargeScreen(true);
+        mPositioner.setBubbleBarLocation(BubbleBarLocation.LEFT);
+        FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
+        mBubbleController.registerBubbleStateListener(bubbleStateListener);
+
+        mEntryListener.onEntryAdded(mRow);
+        assertBarMode();
+
+        mBubbleController.setBubbleBarLocation(BubbleBarLocation.RIGHT,
+                BubbleBarLocation.UpdateSource.DRAG_BAR);
+
+        verify(mBubbleLogger).log(BubbleLogger.Event.BUBBLE_BAR_MOVED_RIGHT_DRAG_BAR);
+    }
+
+    @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
+    @Test
+    public void testEventLogging_bubbleBar_dragBubbleLeft() {
+        mBubbleProperties.mIsBubbleBarEnabled = true;
+        mPositioner.setIsLargeScreen(true);
+        mPositioner.setBubbleBarLocation(BubbleBarLocation.RIGHT);
+        FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
+        mBubbleController.registerBubbleStateListener(bubbleStateListener);
+
+        mEntryListener.onEntryAdded(mRow);
+        assertBarMode();
+
+        mBubbleController.setBubbleBarLocation(BubbleBarLocation.LEFT,
+                BubbleBarLocation.UpdateSource.DRAG_BUBBLE);
+
+        verify(mBubbleLogger).log(BubbleLogger.Event.BUBBLE_BAR_MOVED_LEFT_DRAG_BUBBLE);
+    }
+
+    @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
+    @Test
+    public void testEventLogging_bubbleBar_dragBubbleRight() {
+        mBubbleProperties.mIsBubbleBarEnabled = true;
+        mPositioner.setIsLargeScreen(true);
+        mPositioner.setBubbleBarLocation(BubbleBarLocation.LEFT);
+        FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
+        mBubbleController.registerBubbleStateListener(bubbleStateListener);
+
+        mEntryListener.onEntryAdded(mRow);
+        assertBarMode();
+
+        mBubbleController.setBubbleBarLocation(BubbleBarLocation.RIGHT,
+                BubbleBarLocation.UpdateSource.DRAG_BUBBLE);
+
+        verify(mBubbleLogger).log(BubbleLogger.Event.BUBBLE_BAR_MOVED_RIGHT_DRAG_BUBBLE);
+    }
+
+    @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
+    @Test
     public void testEventLogging_bubbleBar_expandAndCollapse() {
         mBubbleProperties.mIsBubbleBarEnabled = true;
         mPositioner.setIsLargeScreen(true);
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
index 219794f..a7917a0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
@@ -37,9 +37,7 @@
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.currentTime
 
-class FakeAuthenticationRepository(
-    private val currentTime: () -> Long,
-) : AuthenticationRepository {
+class FakeAuthenticationRepository(private val currentTime: () -> Long) : AuthenticationRepository {
 
     override val hintedPinLength: Int = HINTING_PIN_LENGTH
 
@@ -72,6 +70,9 @@
 
     private val credentialCheckingMutex = Mutex(locked = false)
 
+    var maximumTimeToLock: Long = 0
+    var powerButtonInstantlyLocks: Boolean = true
+
     override suspend fun getAuthenticationMethod(): AuthenticationMethodModel {
         return authenticationMethod.value
     }
@@ -114,6 +115,7 @@
         MAX_FAILED_AUTH_TRIES_BEFORE_WIPE
 
     var profileWithMinFailedUnlockAttemptsForWipe: Int = UserHandle.USER_SYSTEM
+
     override suspend fun getProfileWithMinFailedUnlockAttemptsForWipe(): Int =
         profileWithMinFailedUnlockAttemptsForWipe
 
@@ -144,10 +146,7 @@
 
             val failedAttempts = _failedAuthenticationAttempts.value
             if (isSuccessful || failedAttempts < MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) {
-                AuthenticationResultModel(
-                    isSuccessful = isSuccessful,
-                    lockoutDurationMs = 0,
-                )
+                AuthenticationResultModel(isSuccessful = isSuccessful, lockoutDurationMs = 0)
             } else {
                 AuthenticationResultModel(
                     isSuccessful = false,
@@ -178,6 +177,14 @@
         credentialCheckingMutex.unlock()
     }
 
+    override suspend fun getMaximumTimeToLock(): Long {
+        return maximumTimeToLock
+    }
+
+    override suspend fun getPowerButtonInstantlyLocks(): Boolean {
+        return powerButtonInstantlyLocks
+    }
+
     private fun getExpectedCredential(securityMode: SecurityMode): List<Any> {
         return when (val credentialType = getCurrentCredentialType(securityMode)) {
             LockPatternUtils.CREDENTIAL_TYPE_PIN -> credentialOverride ?: DEFAULT_PIN
@@ -219,9 +226,7 @@
         }
 
         @LockPatternUtils.CredentialType
-        private fun getCurrentCredentialType(
-            securityMode: SecurityMode,
-        ): Int {
+        private fun getCurrentCredentialType(securityMode: SecurityMode): Int {
             return when (securityMode) {
                 SecurityMode.PIN,
                 SecurityMode.SimPin,
@@ -260,9 +265,8 @@
 object FakeAuthenticationRepositoryModule {
     @Provides
     @SysUISingleton
-    fun provideFake(
-        scope: TestScope,
-    ) = FakeAuthenticationRepository(currentTime = { scope.currentTime })
+    fun provideFake(scope: TestScope) =
+        FakeAuthenticationRepository(currentTime = { scope.currentTime })
 
     @Module
     interface Bindings {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt
index be84e33..e4c7df6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorKosmos.kt
@@ -19,12 +19,14 @@
 import com.android.systemui.authentication.domain.interactor.authenticationInteractor
 import com.android.systemui.deviceentry.data.repository.deviceEntryRepository
 import com.android.systemui.flags.fakeSystemPropertiesHelper
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.trustInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.lifecycle.activateIn
 import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository
 
 val Kosmos.deviceUnlockedInteractor by Fixture {
     DeviceUnlockedInteractor(
@@ -36,6 +38,8 @@
             powerInteractor = powerInteractor,
             biometricSettingsInteractor = deviceEntryBiometricSettingsInteractor,
             systemPropertiesHelper = fakeSystemPropertiesHelper,
+            userAwareSecureSettingsRepository = userAwareSecureSettingsRepository,
+            keyguardInteractor = keyguardInteractor,
         )
         .apply { activateIn(testScope) }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
index 19e077c..8209ee1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
@@ -87,7 +87,7 @@
     ) : this(
         initInLockscreen = true,
         initiallySendTransitionStepsOnStartTransition = true,
-        testScope
+        testScope,
     )
 
     private val _currentTransitionInfo: MutableStateFlow<TransitionInfo> =
@@ -191,12 +191,12 @@
         if (lastStep != null && lastStep.transitionState != TransitionState.FINISHED) {
             sendTransitionStep(
                 step =
-                TransitionStep(
-                    transitionState = TransitionState.CANCELED,
-                    from = lastStep.from,
-                    to = lastStep.to,
-                    value = 0f,
-                )
+                    TransitionStep(
+                        transitionState = TransitionState.CANCELED,
+                        from = lastStep.from,
+                        to = lastStep.to,
+                        value = 0f,
+                    )
             )
             testScheduler.runCurrent()
         }
@@ -390,6 +390,18 @@
         @FloatRange(from = 0.0, to = 1.0) value: Float,
         state: TransitionState,
     ) = Unit
+
+    override suspend fun forceFinishCurrentTransition() {
+        _transitions.tryEmit(
+            TransitionStep(
+                _currentTransitionInfo.value.from,
+                _currentTransitionInfo.value.to,
+                1f,
+                TransitionState.FINISHED,
+                ownerName = _currentTransitionInfo.value.ownerName,
+            )
+        )
+    }
 }
 
 @Module
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt
index aa94c36..b9a831f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt
@@ -19,6 +19,7 @@
 import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 
 val Kosmos.keyguardTransitionInteractor: KeyguardTransitionInteractor by
@@ -26,6 +27,7 @@
         KeyguardTransitionInteractor(
             scope = applicationCoroutineScope,
             repository = keyguardTransitionRepository,
-            sceneInteractor = sceneInteractor
+            sceneInteractor = sceneInteractor,
+            powerInteractor = powerInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryKosmos.kt
index f49e377..b3be2c0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactoryKosmos.kt
@@ -22,6 +22,7 @@
 import android.os.looper
 import androidx.media3.session.CommandButton
 import androidx.media3.session.MediaController
+import androidx.media3.session.SessionCommand
 import androidx.media3.session.SessionToken
 import com.android.systemui.Flags
 import com.android.systemui.graphics.imageLoader
@@ -30,7 +31,11 @@
 import com.android.systemui.media.controls.shared.mediaLogger
 import com.android.systemui.media.controls.util.fakeMediaControllerFactory
 import com.android.systemui.media.controls.util.fakeSessionTokenFactory
+import com.android.systemui.util.concurrency.execution
 import com.google.common.collect.ImmutableList
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doAnswer
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
 
@@ -46,10 +51,22 @@
                 mock<MediaController>().also {
                     whenever(it.customLayout).thenReturn(customLayout)
                     whenever(it.sessionExtras).thenReturn(Bundle())
+                    whenever(it.isCommandAvailable(any())).thenReturn(true)
+                    whenever(it.isSessionCommandAvailable(any<SessionCommand>())).thenReturn(true)
                 }
             fakeMediaControllerFactory.setMedia3Controller(media3Controller)
             fakeSessionTokenFactory.setMedia3SessionToken(mock<SessionToken>())
         }
+
+        val runnableCaptor = argumentCaptor<Runnable>()
+        val handler =
+            mock<Handler> {
+                on { post(runnableCaptor.capture()) } doAnswer
+                    {
+                        runnableCaptor.lastValue.run()
+                        true
+                    }
+            }
         Media3ActionFactory(
             context = applicationContext,
             imageLoader = imageLoader,
@@ -57,7 +74,8 @@
             tokenFactory = fakeSessionTokenFactory,
             logger = mediaLogger,
             looper = looper,
-            handler = Handler(looper),
+            handler = handler,
             bgScope = testScope,
+            execution = execution,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/LargeTileSpanRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/LargeTileSpanRepositoryKosmos.kt
new file mode 100644
index 0000000..a977121
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/LargeTileSpanRepositoryKosmos.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.data.repository
+
+import android.content.res.mainResources
+import com.android.systemui.common.ui.data.repository.configurationRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+
+val Kosmos.largeTileSpanRepository by
+    Kosmos.Fixture {
+        LargeTileSpanRepository(applicationCoroutineScope, mainResources, configurationRepository)
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorKosmos.kt
index 0c62d0e..8d4db8b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorKosmos.kt
@@ -20,6 +20,7 @@
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.log.core.FakeLogBuffer
 import com.android.systemui.qs.panels.data.repository.defaultLargeTilesRepository
+import com.android.systemui.qs.panels.data.repository.largeTileSpanRepository
 import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor
 
 val Kosmos.iconTilesInteractor by
@@ -28,7 +29,8 @@
             defaultLargeTilesRepository,
             currentTilesInteractor,
             qsPreferencesInteractor,
+            largeTileSpanRepository,
             FakeLogBuffer.Factory.create(),
-            applicationCoroutineScope
+            applicationCoroutineScope,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt
index 45aab86..28edae7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarOrchestratorKosmos.kt
@@ -49,6 +49,7 @@
             fakeStatusBarModePerDisplayRepository,
             fakeStatusBarInitializer,
             fakeStatusBarWindowController,
+            applicationCoroutineScope.coroutineContext,
             mockDemoModeController,
             mockPluginDependencyProvider,
             mockAutoHideController,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
index c3c3cce..dae66d4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
@@ -41,6 +41,7 @@
     override val isGsm = MutableStateFlow(false)
     override val cdmaLevel = MutableStateFlow(0)
     override val primaryLevel = MutableStateFlow(0)
+    override val satelliteLevel = MutableStateFlow(0)
     override val dataConnectionState = MutableStateFlow(DataConnectionState.Disconnected)
     override val dataActivityDirection =
         MutableStateFlow(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
diff --git a/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py b/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py
index cee29dc..7a7de35 100755
--- a/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py
+++ b/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py
@@ -47,6 +47,7 @@
 
     # Test to check the generated jar files to the golden output.
     def test_compare_to_golden(self):
+        self.skipTest("test cannot handle multiple images (see b/378470825)")
         files = os.listdir(GOLDEN_DIR)
         files.sort()
 
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 86d3ee6..d4af7b7 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -42,9 +42,11 @@
 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_GESTURE;
 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR;
 import static android.provider.Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED;
+import static android.view.Display.INVALID_DISPLAY;
 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL;
 import static android.view.accessibility.AccessibilityManager.FlashNotificationReason;
 
+import static com.android.hardware.input.Flags.enableTalkbackAndMagnifierKeyGestures;
 import static com.android.hardware.input.Flags.keyboardA11yMouseKeys;
 import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME;
 import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_COMPONENT_NAME;
@@ -55,6 +57,7 @@
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.ALL;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TRIPLETAP;
@@ -111,6 +114,8 @@
 import android.graphics.Region;
 import android.hardware.display.DisplayManager;
 import android.hardware.fingerprint.IFingerprintService;
+import android.hardware.input.InputManager;
+import android.hardware.input.KeyGestureEvent;
 import android.media.AudioManagerInternal;
 import android.net.Uri;
 import android.os.Binder;
@@ -338,6 +343,8 @@
 
     private AlertDialog mEnableTouchExplorationDialog;
 
+    private final InputManager mInputManager;
+
     private AccessibilityInputFilter mInputFilter;
 
     private boolean mHasInputFilter;
@@ -503,6 +510,25 @@
         }
     }
 
+    private InputManager.KeyGestureEventHandler mKeyGestureEventHandler =
+            new InputManager.KeyGestureEventHandler() {
+                @Override
+                public boolean handleKeyGestureEvent(
+                        @NonNull KeyGestureEvent event,
+                        @Nullable IBinder focusedToken) {
+                    return AccessibilityManagerService.this.handleKeyGestureEvent(event);
+                }
+
+                @Override
+                public boolean isKeyGestureSupported(int gestureType) {
+                    return switch (gestureType) {
+                        case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION,
+                             KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK -> true;
+                        default -> false;
+                    };
+                }
+            };
+
     @VisibleForTesting
     AccessibilityManagerService(
             Context context,
@@ -542,6 +568,7 @@
         mUmi = LocalServices.getService(UserManagerInternal.class);
         // TODO(b/255426725): not used on tests
         mVisibleBgUserIds = null;
+        mInputManager = context.getSystemService(InputManager.class);
 
         init();
     }
@@ -583,6 +610,7 @@
                 mUiAutomationManager, this);
         mFlashNotificationsController = new FlashNotificationsController(mContext);
         mUmi = LocalServices.getService(UserManagerInternal.class);
+        mInputManager = context.getSystemService(InputManager.class);
 
         if (UserManager.isVisibleBackgroundUsersEnabled()) {
             mVisibleBgUserIds = new SparseBooleanArray();
@@ -599,6 +627,9 @@
         registerBroadcastReceivers();
         new AccessibilityContentObserver(mMainHandler).register(
                 mContext.getContentResolver());
+        if (enableTalkbackAndMagnifierKeyGestures()) {
+            mInputManager.registerKeyGestureEventHandler(mKeyGestureEventHandler);
+        }
         disableAccessibilityMenuToMigrateIfNeeded();
     }
 
@@ -640,6 +671,79 @@
         return mIsAccessibilityButtonShown;
     }
 
+    @VisibleForTesting
+    boolean handleKeyGestureEvent(KeyGestureEvent event) {
+        final boolean complete =
+                event.getAction() == KeyGestureEvent.ACTION_GESTURE_COMPLETE
+                        && !event.isCancelled();
+        final int gestureType = event.getKeyGestureType();
+        if (!complete) {
+            return false;
+        }
+
+        String targetName;
+        switch (gestureType) {
+            case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION:
+                targetName = MAGNIFICATION_CONTROLLER_NAME;
+                break;
+            case KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK:
+                targetName = mContext.getString(R.string.config_defaultSelectToSpeakService);
+                if (targetName.isEmpty()) {
+                    return false;
+                }
+
+                final ComponentName targetServiceComponent = TextUtils.isEmpty(targetName)
+                        ? null : ComponentName.unflattenFromString(targetName);
+                AccessibilityServiceInfo accessibilityServiceInfo;
+                synchronized (mLock) {
+                    AccessibilityUserState userState = getCurrentUserStateLocked();
+                    accessibilityServiceInfo =
+                            userState.getInstalledServiceInfoLocked(targetServiceComponent);
+                }
+                if (accessibilityServiceInfo == null) {
+                    return false;
+                }
+
+                // Skip enabling if a warning dialog is required for the feature.
+                // TODO(b/377752960): Explore better options to instead show the warning dialog
+                //  in this scenario.
+                if (isAccessibilityServiceWarningRequired(accessibilityServiceInfo)) {
+                    Slog.w(LOG_TAG,
+                            "Accessibility warning is required before this service can be "
+                                    + "activated automatically via KEY_GESTURE shortcut.");
+                    return false;
+                }
+                break;
+            default:
+                return false;
+        }
+
+        List<String> shortcutTargets = getAccessibilityShortcutTargets(
+                KEY_GESTURE);
+        if (!shortcutTargets.contains(targetName)) {
+            int userId;
+            synchronized (mLock) {
+                userId = mCurrentUserId;
+            }
+            // TODO(b/377752960): Add dialog to confirm enabling the service and to
+            //  activate the first time.
+            enableShortcutForTargets(true, UserShortcutType.KEY_GESTURE,
+                    List.of(targetName), userId);
+
+            // Do not perform action on first press since it was just registered. Eventually,
+            // 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;
+        }
+
+        final int displayId = event.getDisplayId() != INVALID_DISPLAY
+                ? event.getDisplayId() : getLastNonProxyTopFocusedDisplayId();
+        performAccessibilityShortcutInternal(displayId, KEY_GESTURE, targetName);
+
+        return true;
+    }
+
     @Override
     public Pair<float[], MagnificationSpec> getWindowTransformationMatrixAndMagnificationSpec(
             int windowId) {
@@ -1224,14 +1328,14 @@
             int displayId = event.getDisplayId();
             final int windowId = event.getWindowId();
             if (windowId != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID
-                    && displayId == Display.INVALID_DISPLAY) {
+                    && displayId == INVALID_DISPLAY) {
                 displayId = mA11yWindowManager.getDisplayIdByUserIdAndWindowId(
                         resolvedUserId, windowId);
                 event.setDisplayId(displayId);
             }
             synchronized (mLock) {
                 if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
-                        && displayId != Display.INVALID_DISPLAY
+                        && displayId != INVALID_DISPLAY
                         && mA11yWindowManager.isTrackingWindowsLocked(displayId)) {
                     shouldComputeWindows = true;
                 }
@@ -3257,6 +3361,7 @@
         updateAccessibilityShortcutTargetsLocked(userState, SOFTWARE);
         updateAccessibilityShortcutTargetsLocked(userState, GESTURE);
         updateAccessibilityShortcutTargetsLocked(userState, QUICK_SETTINGS);
+        updateAccessibilityShortcutTargetsLocked(userState, KEY_GESTURE);
         // Update the capabilities before the mode because we will check the current mode is
         // invalid or not..
         updateMagnificationCapabilitiesSettingsChangeLocked(userState);
@@ -3387,6 +3492,7 @@
         somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, QUICK_SETTINGS);
         somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, SOFTWARE);
         somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, GESTURE);
+        somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, KEY_GESTURE);
         somethingChanged |= readAccessibilityButtonTargetComponentLocked(userState);
         somethingChanged |= readUserRecommendedUiTimeoutSettingsLocked(userState);
         somethingChanged |= readMagnificationModeForDefaultDisplayLocked(userState);
@@ -3968,6 +4074,7 @@
         if (android.provider.Flags.a11yStandaloneGestureEnabled()) {
             shortcutTypes.add(GESTURE);
         }
+        shortcutTypes.add(KEY_GESTURE);
 
         final ComponentName serviceName = service.getComponentName();
         for (Integer shortcutType: shortcutTypes) {
@@ -4078,13 +4185,15 @@
      */
     private void performAccessibilityShortcutInternal(int displayId,
             @UserShortcutType int shortcutType, @Nullable String targetName) {
-        final List<String> shortcutTargets = getAccessibilityShortcutTargetsInternal(shortcutType);
+        final List<String> shortcutTargets = getAccessibilityShortcutTargetsInternal(
+                shortcutType);
         if (shortcutTargets.isEmpty()) {
             Slog.d(LOG_TAG, "No target to perform shortcut, shortcutType=" + shortcutType);
             return;
         }
         // In case the caller specified a target name
-        if (targetName != null && !doesShortcutTargetsStringContain(shortcutTargets, targetName)) {
+        if (targetName != null && !doesShortcutTargetsStringContain(shortcutTargets,
+                targetName)) {
             Slog.v(LOG_TAG, "Perform shortcut failed, invalid target name:" + targetName);
             targetName = null;
         }
@@ -4306,6 +4415,13 @@
             return;
         }
 
+        if (shortcutType == UserShortcutType.KEY_GESTURE
+                && !enableTalkbackAndMagnifierKeyGestures()) {
+            Slog.w(LOG_TAG,
+                    "KEY_GESTURE type shortcuts are disabled by feature flag");
+            return;
+        }
+
         final String shortcutTypeSettingKey = ShortcutUtils.convertToKey(shortcutType);
         if (shortcutType == UserShortcutType.TRIPLETAP
                 || shortcutType == UserShortcutType.TWOFINGER_DOUBLETAP) {
@@ -5683,6 +5799,9 @@
         private final Uri mAccessibilityGestureTargetsUri = Settings.Secure.getUriFor(
                 Settings.Secure.ACCESSIBILITY_GESTURE_TARGETS);
 
+        private final Uri mAccessibilityKeyGestureTargetsUri = Settings.Secure.getUriFor(
+                Settings.Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS);
+
         private final Uri mUserNonInteractiveUiTimeoutUri = Settings.Secure.getUriFor(
                 Settings.Secure.ACCESSIBILITY_NON_INTERACTIVE_UI_TIMEOUT_MS);
 
@@ -5747,6 +5866,8 @@
             contentResolver.registerContentObserver(
                     mAccessibilityGestureTargetsUri, false, this, UserHandle.USER_ALL);
             contentResolver.registerContentObserver(
+                    mAccessibilityKeyGestureTargetsUri, false, this, UserHandle.USER_ALL);
+            contentResolver.registerContentObserver(
                     mUserNonInteractiveUiTimeoutUri, false, this, UserHandle.USER_ALL);
             contentResolver.registerContentObserver(
                     mUserInteractiveUiTimeoutUri, false, this, UserHandle.USER_ALL);
@@ -5828,6 +5949,10 @@
                     if (readAccessibilityShortcutTargetsLocked(userState, GESTURE)) {
                         onUserStateChangedLocked(userState);
                     }
+                } else if (mAccessibilityKeyGestureTargetsUri.equals(uri)) {
+                    if (readAccessibilityShortcutTargetsLocked(userState, KEY_GESTURE)) {
+                        onUserStateChangedLocked(userState);
+                    }
                 } else if (mUserNonInteractiveUiTimeoutUri.equals(uri)
                         || mUserInteractiveUiTimeoutUri.equals(uri)) {
                     readUserRecommendedUiTimeoutSettingsLocked(userState);
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
index 67b4063..8b3e63d 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
@@ -29,6 +29,7 @@
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TRIPLETAP;
@@ -209,6 +210,7 @@
         mShortcutTargets.put(SOFTWARE, new ArraySet<>());
         mShortcutTargets.put(GESTURE, new ArraySet<>());
         mShortcutTargets.put(QUICK_SETTINGS, new ArraySet<>());
+        mShortcutTargets.put(KEY_GESTURE, new ArraySet<>());
     }
 
     boolean isHandlingAccessibilityEventsLocked() {
diff --git a/services/core/java/com/android/server/BootReceiver.java b/services/core/java/com/android/server/BootReceiver.java
index 23cee9d..1588e04 100644
--- a/services/core/java/com/android/server/BootReceiver.java
+++ b/services/core/java/com/android/server/BootReceiver.java
@@ -53,6 +53,7 @@
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.FileInputStream;
@@ -400,9 +401,18 @@
             Slog.w(TAG, "Tombstone too large to add to DropBox: " + tombstone.toPath());
             return;
         }
-        // Read the proto tombstone file as bytes.
-        final byte[] tombstoneBytes = Files.readAllBytes(tombstone.toPath());
 
+        // Read the proto tombstone file as bytes.
+        // Previously used Files.readAllBytes() which internally creates a ThreadLocal BufferCache
+        // via ChannelInputStream that isn't properly released. Switched to
+        // FileInputStream.transferTo() which avoids the NIO channels completely,
+        // preventing the memory leak while maintaining the same functionality.
+        final byte[] tombstoneBytes;
+        try (FileInputStream fis = new FileInputStream(tombstone);
+                ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+            fis.transferTo(baos);
+            tombstoneBytes = baos.toByteArray();
+        }
         final File tombstoneProtoWithHeaders = File.createTempFile(
                 tombstone.getName(), ".tmp", TOMBSTONE_TMP_DIR);
         Files.setPosixFilePermissions(
diff --git a/services/core/java/com/android/server/SystemConfig.java b/services/core/java/com/android/server/SystemConfig.java
index 1082e62..99772c3 100644
--- a/services/core/java/com/android/server/SystemConfig.java
+++ b/services/core/java/com/android/server/SystemConfig.java
@@ -47,6 +47,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.pm.RoSystemFeatures;
+import com.android.internal.pm.pkg.parsing.ParsingPackageUtils;
 import com.android.internal.util.XmlUtils;
 import com.android.modules.utils.build.UnboundedSdkLevel;
 import com.android.server.pm.permission.PermissionAllowlist;
@@ -2000,6 +2001,13 @@
 
     private void readSplitPermission(XmlPullParser parser, File permFile)
             throws IOException, XmlPullParserException {
+        // If trunkstable feature flag disabled for this split permission, skip this tag.
+        if (ParsingPackageUtils.getAconfigFlags()
+            .skipCurrentElement(/* pkg= */ null, parser, /* allowNoNamespace= */ true)) {
+            XmlUtils.skipCurrentTag(parser);
+            return;
+        }
+
         String splitPerm = parser.getAttributeValue(null, "name");
         if (splitPerm == null) {
             Slog.w(TAG, "<split-permission> without name in " + permFile + " at "
diff --git a/services/core/java/com/android/server/am/BroadcastController.java b/services/core/java/com/android/server/am/BroadcastController.java
index b0f88071..8a12858 100644
--- a/services/core/java/com/android/server/am/BroadcastController.java
+++ b/services/core/java/com/android/server/am/BroadcastController.java
@@ -593,7 +593,7 @@
                             originalStickyCallingUid, BackgroundStartPrivileges.NONE,
                             false /* only PRE_BOOT_COMPLETED should be exempt, no stickies */,
                             null /* filterExtrasForReceiver */,
-                            broadcast.originalCallingAppProcessState);
+                            broadcast.originalCallingAppProcessState, mService.mPlatformCompat);
                     queue.enqueueBroadcastLocked(r);
                 }
             }
@@ -1631,7 +1631,7 @@
                     receivers, resultToApp, resultTo, resultCode, resultData, resultExtras,
                     ordered, sticky, false, userId,
                     backgroundStartPrivileges, timeoutExempt, filterExtrasForReceiver,
-                    callerAppProcessState);
+                    callerAppProcessState, mService.mPlatformCompat);
             broadcastSentEventRecord.setBroadcastRecord(r);
 
             if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Enqueueing ordered broadcast " + r);
diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java
index f908c67..38df10a 100644
--- a/services/core/java/com/android/server/am/BroadcastRecord.java
+++ b/services/core/java/com/android/server/am/BroadcastRecord.java
@@ -42,6 +42,8 @@
 import android.app.BackgroundStartPrivileges;
 import android.app.BroadcastOptions;
 import android.app.BroadcastOptions.DeliveryGroupPolicy;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledSince;
 import android.content.ComponentName;
 import android.content.IIntentReceiver;
 import android.content.Intent;
@@ -55,10 +57,12 @@
 import android.util.ArrayMap;
 import android.util.IntArray;
 import android.util.PrintWriterPrinter;
+import android.util.SparseBooleanArray;
 import android.util.TimeUtils;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.compat.PlatformCompat;
 
 import dalvik.annotation.optimization.NeverCompile;
 
@@ -77,6 +81,15 @@
  * An active intent broadcast.
  */
 final class BroadcastRecord extends Binder {
+    /**
+     * Limit the scope of the priority values to the process level. This means that priority values
+     * will only influence the order of broadcast delivery within the same process.
+     */
+    @ChangeId
+    @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.BASE)
+    @VisibleForTesting
+    static final long CHANGE_LIMIT_PRIORITY_SCOPE = 371307720L;
+
     final @NonNull Intent intent;    // the original intent that generated us
     final @Nullable ComponentName targetComp; // original component name set on the intent
     final @Nullable ProcessRecord callerApp; // process that sent this
@@ -417,13 +430,13 @@
             @NonNull BackgroundStartPrivileges backgroundStartPrivileges,
             boolean timeoutExempt,
             @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver,
-            int callerAppProcessState) {
+            int callerAppProcessState, PlatformCompat platformCompat) {
         this(queue, intent, callerApp, callerPackage, callerFeatureId, callingPid,
                 callingUid, callerInstantApp, resolvedType, requiredPermissions,
                 excludedPermissions, excludedPackages, appOp, options, receivers, resultToApp,
                 resultTo, resultCode, resultData, resultExtras, serialized, sticky,
                 initialSticky, userId, -1, backgroundStartPrivileges, timeoutExempt,
-                filterExtrasForReceiver, callerAppProcessState);
+                filterExtrasForReceiver, callerAppProcessState, platformCompat);
     }
 
     BroadcastRecord(BroadcastQueue _queue,
@@ -439,7 +452,7 @@
             @NonNull BackgroundStartPrivileges backgroundStartPrivileges,
             boolean timeoutExempt,
             @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver,
-            int callerAppProcessState) {
+            int callerAppProcessState, PlatformCompat platformCompat) {
         if (_intent == null) {
             throw new NullPointerException("Can't construct with a null intent");
         }
@@ -466,7 +479,8 @@
         urgent = calculateUrgent(_intent, _options);
         deferUntilActive = calculateDeferUntilActive(_callingUid,
                 _options, _resultTo, _serialized, urgent);
-        blockedUntilBeyondCount = calculateBlockedUntilBeyondCount(receivers, _serialized);
+        blockedUntilBeyondCount = calculateBlockedUntilBeyondCount(
+                receivers, _serialized, platformCompat);
         scheduledTime = new long[delivery.length];
         terminalTime = new long[delivery.length];
         resultToApp = _resultToApp;
@@ -730,7 +744,8 @@
     }
 
     /**
-     * Determine if the result of {@link #calculateBlockedUntilBeyondCount(List, boolean)}
+     * Determine if the result of
+     * {@link #calculateBlockedUntilBeyondCount(List, boolean, PlatformCompat)}
      * has prioritized tranches of receivers.
      */
     @VisibleForTesting
@@ -754,37 +769,121 @@
      */
     @VisibleForTesting
     static @NonNull int[] calculateBlockedUntilBeyondCount(
-            @NonNull List<Object> receivers, boolean ordered) {
+            @NonNull List<Object> receivers, boolean ordered, PlatformCompat platformCompat) {
         final int N = receivers.size();
         final int[] blockedUntilBeyondCount = new int[N];
-        int lastPriority = 0;
-        int lastPriorityIndex = 0;
-        for (int i = 0; i < N; i++) {
-            if (ordered) {
-                // When sending an ordered broadcast, we need to block this
-                // receiver until all previous receivers have terminated
+        if (ordered) {
+            // When sending an ordered broadcast, we need to block this
+            // receiver until all previous receivers have terminated
+            for (int i = 0; i < N; i++) {
                 blockedUntilBeyondCount[i] = i;
+            }
+        } else {
+            if (Flags.limitPriorityScope()) {
+                final boolean[] changeEnabled = calculateChangeStateForReceivers(
+                        receivers, CHANGE_LIMIT_PRIORITY_SCOPE, platformCompat);
+
+                // Priority of the previous tranche
+                int lastTranchePriority = 0;
+                // Priority of the current tranche
+                int currentTranchePriority = 0;
+                // Index of the last receiver in the previous tranche
+                int lastTranchePriorityIndex = -1;
+                // Index of the last receiver with change disabled in the previous tranche
+                int lastTrancheChangeDisabledIndex = -1;
+                // Index of the last receiver with change disabled in the current tranche
+                int currentTrancheChangeDisabledIndex = -1;
+
+                for (int i = 0; i < N; i++) {
+                    final int thisPriority = getReceiverPriority(receivers.get(i));
+                    if (i == 0) {
+                        currentTranchePriority = thisPriority;
+                        if (!changeEnabled[i]) {
+                            currentTrancheChangeDisabledIndex = i;
+                        }
+                        continue;
+                    }
+
+                    // Check if a new priority tranche has started
+                    if (thisPriority != currentTranchePriority) {
+                        // Update tranche boundaries and reset the disabled index.
+                        if (currentTrancheChangeDisabledIndex != -1) {
+                            lastTrancheChangeDisabledIndex = currentTrancheChangeDisabledIndex;
+                        }
+                        lastTranchePriority = currentTranchePriority;
+                        lastTranchePriorityIndex = i - 1;
+                        currentTranchePriority = thisPriority;
+                        currentTrancheChangeDisabledIndex = -1;
+                    }
+                    if (!changeEnabled[i]) {
+                        currentTrancheChangeDisabledIndex = i;
+
+                        // Since the change is disabled, block the current receiver until the
+                        // last receiver in the previous tranche.
+                        blockedUntilBeyondCount[i] = lastTranchePriorityIndex + 1;
+                    } else if (thisPriority != lastTranchePriority) {
+                        // If the changeId was disabled for an earlier receiver and the current
+                        // receiver has a different priority, block the current receiver
+                        // until that earlier receiver.
+                        if (lastTrancheChangeDisabledIndex != -1) {
+                            blockedUntilBeyondCount[i] = lastTrancheChangeDisabledIndex + 1;
+                        }
+                    }
+                }
+                // If the entire list is in the same priority tranche or no receivers had
+                // changeId disabled, mark as -1 to indicate that none of them need to wait
+                if (N > 0 && (lastTranchePriorityIndex == -1
+                        || (lastTrancheChangeDisabledIndex == -1
+                                && currentTrancheChangeDisabledIndex == -1))) {
+                    Arrays.fill(blockedUntilBeyondCount, -1);
+                }
             } else {
                 // When sending a prioritized broadcast, we only need to wait
                 // for the previous tranche of receivers to be terminated
-                final int thisPriority = getReceiverPriority(receivers.get(i));
-                if ((i == 0) || (thisPriority != lastPriority)) {
-                    lastPriority = thisPriority;
-                    lastPriorityIndex = i;
-                    blockedUntilBeyondCount[i] = i;
-                } else {
-                    blockedUntilBeyondCount[i] = lastPriorityIndex;
+                int lastPriority = 0;
+                int lastPriorityIndex = 0;
+                for (int i = 0; i < N; i++) {
+                    final int thisPriority = getReceiverPriority(receivers.get(i));
+                    if ((i == 0) || (thisPriority != lastPriority)) {
+                        lastPriority = thisPriority;
+                        lastPriorityIndex = i;
+                        blockedUntilBeyondCount[i] = i;
+                    } else {
+                        blockedUntilBeyondCount[i] = lastPriorityIndex;
+                    }
+                }
+                // If the entire list is in the same priority tranche, mark as -1 to
+                // indicate that none of them need to wait
+                if (N > 0 && blockedUntilBeyondCount[N - 1] == 0) {
+                    Arrays.fill(blockedUntilBeyondCount, -1);
                 }
             }
         }
-        // If the entire list is in the same priority tranche, mark as -1 to
-        // indicate that none of them need to wait
-        if (N > 0 && blockedUntilBeyondCount[N - 1] == 0) {
-            Arrays.fill(blockedUntilBeyondCount, -1);
-        }
         return blockedUntilBeyondCount;
     }
 
+    @VisibleForTesting
+    static @NonNull boolean[] calculateChangeStateForReceivers(@NonNull List<Object> receivers,
+            long changeId, PlatformCompat platformCompat) {
+        final SparseBooleanArray changeStateForUids = new SparseBooleanArray();
+        final int count = receivers.size();
+        final boolean[] changeStateForReceivers = new boolean[count];
+        for (int i = 0; i < count; ++i) {
+            final int receiverUid = getReceiverUid(receivers.get(i));
+            final boolean isChangeEnabled;
+            final int idx = changeStateForUids.indexOfKey(receiverUid);
+            if (idx >= 0) {
+                isChangeEnabled = changeStateForUids.valueAt(idx);
+            } else {
+                isChangeEnabled = platformCompat.isChangeEnabledByUidInternalNoLogging(
+                        changeId, receiverUid);
+                changeStateForUids.put(receiverUid, isChangeEnabled);
+            }
+            changeStateForReceivers[i] = isChangeEnabled;
+        }
+        return changeStateForReceivers;
+    }
+
     static int getReceiverUid(@NonNull Object receiver) {
         if (receiver instanceof BroadcastFilter) {
             return ((BroadcastFilter) receiver).owningUid;
diff --git a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
index f4a931f..d2af84c 100644
--- a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
+++ b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
@@ -19,7 +19,6 @@
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_PERMISSIONS_REVIEW;
 import static com.android.server.am.ActivityManagerService.checkComponentPermission;
 import static com.android.server.am.BroadcastQueue.TAG;
-import static com.android.server.am.Flags.usePermissionManagerForBroadcastDeliveryCheck;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -289,33 +288,16 @@
 
         if (info.activityInfo.applicationInfo.uid != Process.SYSTEM_UID &&
                 r.requiredPermissions != null && r.requiredPermissions.length > 0) {
-            final AttributionSource[] attributionSources;
-            if (usePermissionManagerForBroadcastDeliveryCheck()) {
-                attributionSources = createAttributionSourcesForResolveInfo(info);
-            } else {
-                attributionSources = null;
-            }
+            final AttributionSource[] attributionSources =
+                    createAttributionSourcesForResolveInfo(info);
             for (int i = 0; i < r.requiredPermissions.length; i++) {
                 String requiredPermission = r.requiredPermissions[i];
-                try {
-                    if (usePermissionManagerForBroadcastDeliveryCheck()) {
-                        perm = hasPermissionForDataDelivery(
-                                requiredPermission,
-                                "Broadcast delivered to " + info.activityInfo.name,
-                                attributionSources)
-                                        ? PackageManager.PERMISSION_GRANTED
-                                        : PackageManager.PERMISSION_DENIED;
-                    } else {
-                        perm = AppGlobals.getPackageManager()
-                                .checkPermission(
-                                        requiredPermission,
-                                        info.activityInfo.applicationInfo.packageName,
-                                        UserHandle
-                                                .getUserId(info.activityInfo.applicationInfo.uid));
-                    }
-                } catch (RemoteException e) {
-                    perm = PackageManager.PERMISSION_DENIED;
-                }
+                perm = hasPermissionForDataDelivery(
+                        requiredPermission,
+                        "Broadcast delivered to " + info.activityInfo.name,
+                        attributionSources)
+                                ? PackageManager.PERMISSION_GRANTED
+                                : PackageManager.PERMISSION_DENIED;
                 if (perm != PackageManager.PERMISSION_GRANTED) {
                     return "Permission Denial: receiving "
                             + r.intent + " to "
@@ -324,15 +306,6 @@
                             + " due to sender " + r.callerPackage
                             + " (uid " + r.callingUid + ")";
                 }
-                if (!usePermissionManagerForBroadcastDeliveryCheck()) {
-                    int appOp = AppOpsManager.permissionToOpCode(requiredPermission);
-                    if (appOp != AppOpsManager.OP_NONE && appOp != r.appOp) {
-                        if (!noteOpForManifestReceiver(appOp, r, info, component)) {
-                            return "Skipping delivery to " + info.activityInfo.packageName
-                                    + " due to required appop " + appOp;
-                        }
-                    }
-                }
             }
         }
         if (r.appOp != AppOpsManager.OP_NONE) {
@@ -452,35 +425,20 @@
 
         // Check that the receiver has the required permission(s) to receive this broadcast.
         if (r.requiredPermissions != null && r.requiredPermissions.length > 0) {
-            final AttributionSource attributionSource;
-            if (usePermissionManagerForBroadcastDeliveryCheck()) {
-                attributionSource =
-                        new AttributionSource.Builder(filter.receiverList.uid)
-                                .setPid(filter.receiverList.pid)
-                                .setPackageName(filter.packageName)
-                                .setAttributionTag(filter.featureId)
-                                .build();
-            } else {
-                attributionSource = null;
-            }
+            final AttributionSource attributionSource =
+                    new AttributionSource.Builder(filter.receiverList.uid)
+                            .setPid(filter.receiverList.pid)
+                            .setPackageName(filter.packageName)
+                            .setAttributionTag(filter.featureId)
+                            .build();
             for (int i = 0; i < r.requiredPermissions.length; i++) {
                 String requiredPermission = r.requiredPermissions[i];
-                final int perm;
-                if (usePermissionManagerForBroadcastDeliveryCheck()) {
-                    perm = hasPermissionForDataDelivery(
-                            requiredPermission,
-                            "Broadcast delivered to registered receiver " + filter.receiverId,
-                            attributionSource)
-                                    ? PackageManager.PERMISSION_GRANTED
-                                    : PackageManager.PERMISSION_DENIED;
-                } else {
-                    perm = checkComponentPermission(
-                            requiredPermission,
-                            filter.receiverList.pid,
-                            filter.receiverList.uid,
-                            -1 /* owningUid */,
-                            true /* exported */);
-                }
+                final int perm = hasPermissionForDataDelivery(
+                        requiredPermission,
+                        "Broadcast delivered to registered receiver " + filter.receiverId,
+                        attributionSource)
+                                ? PackageManager.PERMISSION_GRANTED
+                                : PackageManager.PERMISSION_DENIED;
                 if (perm != PackageManager.PERMISSION_GRANTED) {
                     return "Permission Denial: receiving "
                             + r.intent.toString()
@@ -491,24 +449,6 @@
                             + " due to sender " + r.callerPackage
                             + " (uid " + r.callingUid + ")";
                 }
-                if (!usePermissionManagerForBroadcastDeliveryCheck()) {
-                    int appOp = AppOpsManager.permissionToOpCode(requiredPermission);
-                    if (appOp != AppOpsManager.OP_NONE && appOp != r.appOp
-                            && mService.getAppOpsManager().noteOpNoThrow(appOp,
-                            filter.receiverList.uid, filter.packageName, filter.featureId,
-                            "Broadcast delivered to registered receiver " + filter.receiverId)
-                            != AppOpsManager.MODE_ALLOWED) {
-                        return "Appop Denial: receiving "
-                                + r.intent.toString()
-                                + " to " + filter.receiverList.app
-                                + " (pid=" + filter.receiverList.pid
-                                + ", uid=" + filter.receiverList.uid + ")"
-                                + " requires appop " + AppOpsManager.permissionToOp(
-                                requiredPermission)
-                                + " due to sender " + r.callerPackage
-                                + " (uid " + r.callingUid + ")";
-                    }
-                }
             }
         }
         if ((r.requiredPermissions == null || r.requiredPermissions.length == 0)) {
diff --git a/services/core/java/com/android/server/am/broadcasts_flags.aconfig b/services/core/java/com/android/server/am/broadcasts_flags.aconfig
index b1185d5..7f169db 100644
--- a/services/core/java/com/android/server/am/broadcasts_flags.aconfig
+++ b/services/core/java/com/android/server/am/broadcasts_flags.aconfig
@@ -7,4 +7,12 @@
     description: "Restrict priority values defined by non-system apps"
     is_fixed_read_only: true
     bug: "369487976"
+}
+
+flag {
+    name: "limit_priority_scope"
+    namespace: "backstage_power"
+    description: "Limit the scope of receiver priorities to within a process"
+    is_fixed_read_only: true
+    bug: "369487976"
 }
\ No newline at end of file
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 09de894..34d4fb0 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -40,6 +40,7 @@
 import android.bluetooth.BluetoothLeAudio;
 import android.bluetooth.BluetoothProfile;
 import android.content.Intent;
+import android.media.AudioDescriptor;
 import android.media.AudioDeviceAttributes;
 import android.media.AudioDeviceInfo;
 import android.media.AudioDevicePort;
@@ -47,6 +48,7 @@
 import android.media.AudioManager;
 import android.media.AudioManager.AudioDeviceCategory;
 import android.media.AudioPort;
+import android.media.AudioProfile;
 import android.media.AudioRoutesInfo;
 import android.media.AudioSystem;
 import android.media.IAudioRoutesObserver;
@@ -619,6 +621,8 @@
         final int mGroupId;
         @NonNull String mPeerDeviceAddress;
         @NonNull String mPeerIdentityDeviceAddress;
+        @NonNull List<AudioProfile> mAudioProfiles;
+        @NonNull List<AudioDescriptor> mAudioDescriptors;
 
         /** Disabled operating modes for this device. Use a negative logic so that by default
          * an empty list means all modes are allowed.
@@ -627,7 +631,8 @@
 
         DeviceInfo(int deviceType, String deviceName, String address,
                    String identityAddress, int codecFormat,
-                   int groupId, String peerAddress, String peerIdentityAddress) {
+                   int groupId, String peerAddress, String peerIdentityAddress,
+                   List<AudioProfile> profiles, List<AudioDescriptor> descriptors) {
             mDeviceType = deviceType;
             mDeviceName = TextUtils.emptyIfNull(deviceName);
             mDeviceAddress = TextUtils.emptyIfNull(address);
@@ -639,6 +644,16 @@
             mGroupId = groupId;
             mPeerDeviceAddress = TextUtils.emptyIfNull(peerAddress);
             mPeerIdentityDeviceAddress = TextUtils.emptyIfNull(peerIdentityAddress);
+            mAudioProfiles = profiles;
+            mAudioDescriptors = descriptors;
+        }
+
+        DeviceInfo(int deviceType, String deviceName, String address,
+                   String identityAddress, int codecFormat,
+                   int groupId, String peerAddress, String peerIdentityAddress) {
+            this(deviceType, deviceName, address, identityAddress, codecFormat,
+                    groupId, peerAddress, peerIdentityAddress,
+                    new ArrayList<>(), new ArrayList<>());
         }
 
         /** Constructor for all devices except A2DP sink and LE Audio */
@@ -646,6 +661,13 @@
             this(deviceType, deviceName, address, null, AudioSystem.AUDIO_FORMAT_DEFAULT);
         }
 
+        /** Constructor for HDMI OUT, HDMI ARC/EARC sink devices */
+        DeviceInfo(int deviceType, String deviceName, String address,
+            List<AudioProfile> profiles, List<AudioDescriptor> descriptors) {
+            this(deviceType, deviceName, address, null, AudioSystem.AUDIO_FORMAT_DEFAULT,
+                BluetoothLeAudio.GROUP_ID_INVALID, null, null, profiles, descriptors);
+        }
+
         /** Constructor for A2DP sink devices */
         DeviceInfo(int deviceType, String deviceName, String address,
                    String identityAddress, int codecFormat) {
@@ -1194,27 +1216,31 @@
     }
 
     /*package*/ void onToggleHdmi() {
-        MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "onToggleHdmi")
-                .set(MediaMetrics.Property.DEVICE,
-                        AudioSystem.getDeviceName(AudioSystem.DEVICE_OUT_HDMI));
+        final int[] hdmiDevices = { AudioSystem.DEVICE_OUT_HDMI,
+                AudioSystem.DEVICE_OUT_HDMI_ARC, AudioSystem.DEVICE_OUT_HDMI_EARC };
+
         synchronized (mDevicesLock) {
-            // Is HDMI connected?
-            final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HDMI, "");
-            final DeviceInfo di = mConnectedDevices.get(key);
-            if (di == null) {
-                Log.e(TAG, "invalid null DeviceInfo in onToggleHdmi");
-                mmi.set(MediaMetrics.Property.EARLY_RETURN, "invalid null DeviceInfo").record();
-                return;
+            for (DeviceInfo di : mConnectedDevices.values()) {
+                boolean isHdmiDevice = Arrays.stream(hdmiDevices).anyMatch(device ->
+                    device == di.mDeviceType);
+                if (isHdmiDevice) {
+                    MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "onToggleHdmi")
+                            .set(MediaMetrics.Property.DEVICE,
+                                    AudioSystem.getDeviceName(di.mDeviceType));
+                    AudioDeviceAttributes ada = new AudioDeviceAttributes(
+                            AudioDeviceAttributes.ROLE_OUTPUT,
+                            AudioDeviceInfo.convertInternalDeviceToDeviceType(di.mDeviceType),
+                            di.mDeviceAddress, di.mDeviceName, di.mAudioProfiles,
+                            di.mAudioDescriptors);
+                    // Toggle HDMI to retrigger broadcast with proper formats.
+                    setWiredDeviceConnectionState(ada,
+                            AudioSystem.DEVICE_STATE_UNAVAILABLE, "onToggleHdmi"); // disconnect
+                    setWiredDeviceConnectionState(ada,
+                            AudioSystem.DEVICE_STATE_AVAILABLE, "onToggleHdmi"); // reconnect
+                    mmi.record();
+                }
             }
-            // Toggle HDMI to retrigger broadcast with proper formats.
-            setWiredDeviceConnectionState(
-                    new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_HDMI, ""),
-                    AudioSystem.DEVICE_STATE_UNAVAILABLE, "android"); // disconnect
-            setWiredDeviceConnectionState(
-                    new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_HDMI, ""),
-                    AudioSystem.DEVICE_STATE_AVAILABLE, "android"); // reconnect
         }
-        mmi.record();
     }
 
     @GuardedBy("mDevicesLock")
@@ -1818,7 +1844,15 @@
                             .printSlog(EventLogger.Event.ALOGE, TAG));
                     return false;
                 }
-                mConnectedDevices.put(deviceKey, new DeviceInfo(device, deviceName, address));
+
+                if (device == AudioSystem.DEVICE_OUT_HDMI ||
+                    device == AudioSystem.DEVICE_OUT_HDMI_ARC ||
+                    device == AudioSystem.DEVICE_OUT_HDMI_EARC) {
+                    mConnectedDevices.put(deviceKey, new DeviceInfo(device, deviceName,
+                        address, attributes.getAudioProfiles(), attributes.getAudioDescriptors()));
+                } else {
+                    mConnectedDevices.put(deviceKey, new DeviceInfo(device, deviceName, address));
+                }
                 mDeviceBroker.postAccessoryPlugMediaUnmute(device);
                 status = true;
             } else if (!connect && isConnected) {
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index c0aa4cc..71f17d1 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -242,6 +242,11 @@
             Flags::autoBrightnessModeBedtimeWear
     );
 
+    private final FlagState mEnablePluginManagerFlagState = new FlagState(
+            Flags.FLAG_ENABLE_PLUGIN_MANAGER,
+            Flags::enablePluginManager
+    );
+
     /**
      * @return {@code true} if 'port' is allowed in display layout configuration file.
      */
@@ -517,6 +522,10 @@
         return mAutoBrightnessModeBedtimeWearFlagState.isEnabled();
     }
 
+    public boolean isPluginManagerEnabled() {
+        return mEnablePluginManagerFlagState.isEnabled();
+    }
+
     /**
      * dumps all flagstates
      * @param pw printWriter
@@ -568,6 +577,7 @@
         pw.println(" " + mIsUserRefreshRateForExternalDisplayEnabled);
         pw.println(" " + mHasArrSupport);
         pw.println(" " + mAutoBrightnessModeBedtimeWearFlagState);
+        pw.println(" " + mEnablePluginManagerFlagState);
     }
 
     private static class FlagState {
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index a9bdcce..7850360 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -446,3 +446,11 @@
     bug: "365163968"
     is_fixed_read_only: true
 }
+
+flag {
+    name: "enable_plugin_manager"
+    namespace: "display_manager"
+    description: "Flag to enable DisplayManager plugins"
+    bug: "354059797"
+    is_fixed_read_only: true
+}
diff --git a/services/core/java/com/android/server/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java
index 2e7f5c0..6f35402 100644
--- a/services/core/java/com/android/server/input/InputGestureManager.java
+++ b/services/core/java/com/android/server/input/InputGestureManager.java
@@ -221,6 +221,12 @@
             systemShortcuts.add(createKeyGesture(KeyEvent.KEYCODE_EQUALS,
                     KeyEvent.META_META_ON | KeyEvent.META_ALT_ON,
                     KeyGestureEvent.KEY_GESTURE_TYPE_MAGNIFIER_ZOOM_IN));
+            systemShortcuts.add(createKeyGesture(KeyEvent.KEYCODE_M,
+                    KeyEvent.META_META_ON | KeyEvent.META_ALT_ON,
+                    KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION));
+            systemShortcuts.add(createKeyGesture(KeyEvent.KEYCODE_S,
+                    KeyEvent.META_META_ON | KeyEvent.META_ALT_ON,
+                    KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK));
         }
         if (keyboardA11yShortcutControl()) {
             if (InputSettings.isAccessibilityBounceKeysFeatureEnabled()) {
diff --git a/services/core/java/com/android/server/pm/InstallDependencyHelper.java b/services/core/java/com/android/server/pm/InstallDependencyHelper.java
new file mode 100644
index 0000000..745665b
--- /dev/null
+++ b/services/core/java/com/android/server/pm/InstallDependencyHelper.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pm;
+
+import static android.content.pm.PackageManager.INSTALL_FAILED_MISSING_SHARED_LIBRARY;
+
+import android.content.pm.SharedLibraryInfo;
+import android.content.pm.parsing.PackageLite;
+import android.os.OutcomeReceiver;
+
+import java.util.List;
+
+/**
+ * Helper class to interact with SDK Dependency Installer service.
+ */
+public class InstallDependencyHelper {
+    private final SharedLibrariesImpl mSharedLibraries;
+
+    InstallDependencyHelper(SharedLibrariesImpl sharedLibraries) {
+        mSharedLibraries = sharedLibraries;
+    }
+
+    void resolveLibraryDependenciesIfNeeded(PackageLite pkg,
+            OutcomeReceiver<Void, PackageManagerException> callback) {
+        final List<SharedLibraryInfo> missing;
+        try {
+            missing = mSharedLibraries.collectMissingSharedLibraryInfos(pkg);
+        } catch (PackageManagerException e) {
+            callback.onError(e);
+            return;
+        }
+
+        if (missing.isEmpty()) {
+            // No need for dependency resolution. Move to installation directly.
+            callback.onResult(null);
+            return;
+        }
+
+        try {
+            bindToDependencyInstaller();
+        } catch (Exception e) {
+            PackageManagerException pe = new PackageManagerException(
+                    INSTALL_FAILED_MISSING_SHARED_LIBRARY, e.getMessage());
+            callback.onError(pe);
+        }
+    }
+
+    private void bindToDependencyInstaller() {
+        throw new IllegalStateException("Failed to bind to Dependency Installer");
+    }
+
+
+}
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index ef09976..eb70748 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -220,6 +220,7 @@
 
     private AppOpsManager mAppOps;
     private final VerifierController mVerifierController;
+    private final InstallDependencyHelper mInstallDependencyHelper;
 
     private final HandlerThread mInstallThread;
     private final Handler mInstallHandler;
@@ -346,6 +347,8 @@
         synchronized (mVerificationPolicyPerUser) {
             mVerificationPolicyPerUser.put(USER_SYSTEM, DEFAULT_VERIFICATION_POLICY);
         }
+        mInstallDependencyHelper = new InstallDependencyHelper(
+                mPm.mInjector.getSharedLibrariesImpl());
 
         LocalServices.getService(SystemServiceManager.class).startService(
                 new Lifecycle(context, this));
@@ -543,7 +546,7 @@
                             session = PackageInstallerSession.readFromXml(in, mInternalCallback,
                                     mContext, mPm, mInstallThread.getLooper(), mStagingManager,
                                     mSessionsDir, this, mSilentUpdatePolicy,
-                                    mVerifierController);
+                                    mVerifierController, mInstallDependencyHelper);
                         } catch (Exception e) {
                             Slog.e(TAG, "Could not read session", e);
                             continue;
@@ -1065,7 +1068,8 @@
                 userId, callingUid, installSource, params, createdMillis, 0L, stageDir, stageCid,
                 null, null, false, false, false, false, null, SessionInfo.INVALID_ID,
                 false, false, false, PackageManager.INSTALL_UNKNOWN, "", null,
-                mVerifierController, verificationPolicy, verificationPolicy);
+                mVerifierController, verificationPolicy, verificationPolicy,
+                mInstallDependencyHelper);
 
         synchronized (mSessions) {
             mSessions.put(sessionId, session);
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 2a92de5..bad1201 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -145,6 +145,7 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
+import android.os.OutcomeReceiver;
 import android.os.ParcelFileDescriptor;
 import android.os.ParcelableException;
 import android.os.PersistableBundle;
@@ -433,6 +434,8 @@
     private final StagingManager mStagingManager;
     @NonNull private final VerifierController mVerifierController;
 
+    private final InstallDependencyHelper mInstallDependencyHelper;
+
     final int sessionId;
     final int userId;
     final SessionParams params;
@@ -1188,7 +1191,8 @@
             String sessionErrorMessage, DomainSet preVerifiedDomains,
             @NonNull VerifierController verifierController,
             @PackageInstaller.VerificationPolicy int initialVerificationPolicy,
-            @PackageInstaller.VerificationPolicy int currentVerificationPolicy) {
+            @PackageInstaller.VerificationPolicy int currentVerificationPolicy,
+            InstallDependencyHelper installDependencyHelper) {
         mCallback = callback;
         mContext = context;
         mPm = pm;
@@ -1200,6 +1204,7 @@
         mVerifierController = verifierController;
         mInitialVerificationPolicy = initialVerificationPolicy;
         mCurrentVerificationPolicy = new AtomicInteger(currentVerificationPolicy);
+        mInstallDependencyHelper = installDependencyHelper;
 
         this.sessionId = sessionId;
         this.userId = userId;
@@ -2611,6 +2616,13 @@
         maybeFinishChildSessions(error, msg);
     }
 
+    private void onSessionDependencyResolveFailure(int error, String msg) {
+        Slog.e(TAG, "Failed to resolve dependency for session " + sessionId);
+        // Dispatch message to remove session from PackageInstallerService.
+        dispatchSessionFinished(error, msg, null);
+        maybeFinishChildSessions(error, msg);
+    }
+
     private void onSystemDataLoaderUnrecoverable() {
         final String packageName = getPackageName();
         if (TextUtils.isEmpty(packageName)) {
@@ -3402,7 +3414,34 @@
                     /* extras= */ null, /* forPreapproval= */ false);
             return;
         }
-        install();
+
+        if (Flags.sdkDependencyInstaller() && !isMultiPackage()) {
+            resolveLibraryDependenciesIfNeeded();
+        } else {
+            install();
+        }
+    }
+
+
+    private void resolveLibraryDependenciesIfNeeded() {
+        synchronized (mLock) {
+            // TODO(b/372862145): Callback should be called on a handler passed as parameter
+            mInstallDependencyHelper.resolveLibraryDependenciesIfNeeded(mPackageLite,
+                    new OutcomeReceiver<>() {
+
+                        @Override
+                        public void onResult(Void result) {
+                            install();
+                        }
+
+                        @Override
+                        public void onError(@NonNull PackageManagerException e) {
+                            final String completeMsg = ExceptionUtils.getCompleteMessage(e);
+                            setSessionFailed(e.error, completeMsg);
+                            onSessionDependencyResolveFailure(e.error, completeMsg);
+                        }
+                    });
+        }
     }
 
     /**
@@ -6048,7 +6087,8 @@
             @NonNull StagingManager stagingManager, @NonNull File sessionsDir,
             @NonNull PackageSessionProvider sessionProvider,
             @NonNull SilentUpdatePolicy silentUpdatePolicy,
-            @NonNull VerifierController verifierController)
+            @NonNull VerifierController verifierController,
+            @NonNull InstallDependencyHelper installDependencyHelper)
             throws IOException, XmlPullParserException {
         final int sessionId = in.getAttributeInt(null, ATTR_SESSION_ID);
         final int userId = in.getAttributeInt(null, ATTR_USER_ID);
@@ -6257,6 +6297,6 @@
                 stageCid, fileArray, checksumsMap, prepared, committed, destroyed, sealed,
                 childSessionIdsArray, parentSessionId, isReady, isFailed, isApplied,
                 sessionErrorCode, sessionErrorMessage, preVerifiedDomains, verifierController,
-                initialVerificationPolicy, currentVerificationPolicy);
+                initialVerificationPolicy, currentVerificationPolicy, installDependencyHelper);
     }
 }
diff --git a/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java b/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java
index a28e3c1..52e8c52 100644
--- a/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java
+++ b/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java
@@ -38,6 +38,7 @@
 import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
 
 import java.util.ArrayList;
@@ -45,7 +46,8 @@
 
 /** Helper class to handle PackageMonitorCallback and notify the registered client. This is mainly
  * used by PackageMonitor to improve the broadcast latency. */
-class PackageMonitorCallbackHelper {
+@VisibleForTesting
+public class PackageMonitorCallbackHelper {
 
     private static final boolean DEBUG = false;
     private static final String TAG = "PackageMonitorCallbackHelper";
diff --git a/services/core/java/com/android/server/pm/SharedLibrariesImpl.java b/services/core/java/com/android/server/pm/SharedLibrariesImpl.java
index 929fccc..fc54f68 100644
--- a/services/core/java/com/android/server/pm/SharedLibrariesImpl.java
+++ b/services/core/java/com/android/server/pm/SharedLibrariesImpl.java
@@ -33,6 +33,7 @@
 import android.content.pm.Signature;
 import android.content.pm.SigningDetails;
 import android.content.pm.VersionedPackage;
+import android.content.pm.parsing.PackageLite;
 import android.os.Build;
 import android.os.Process;
 import android.os.UserHandle;
@@ -83,6 +84,7 @@
     private static final boolean DEBUG_SHARED_LIBRARIES = false;
 
     private static final String LIBRARY_TYPE_SDK = "sdk";
+    private static final String LIBRARY_TYPE_STATIC = "static shared";
 
     /**
      * Apps targeting Android S and above need to declare dependencies to the public native
@@ -926,18 +928,19 @@
         if (!pkg.getUsesLibraries().isEmpty()) {
             usesLibraryInfos = collectSharedLibraryInfos(pkg.getUsesLibraries(), null, null, null,
                     pkg.getPackageName(), "shared", true, pkg.getTargetSdkVersion(), null,
-                    availablePackages, newLibraries);
+                    availablePackages, newLibraries, null);
         }
         if (!pkg.getUsesStaticLibraries().isEmpty()) {
             usesLibraryInfos = collectSharedLibraryInfos(pkg.getUsesStaticLibraries(),
                     pkg.getUsesStaticLibrariesVersions(), pkg.getUsesStaticLibrariesCertDigests(),
-                    null, pkg.getPackageName(), "static shared", true,
-                    pkg.getTargetSdkVersion(), usesLibraryInfos, availablePackages, newLibraries);
+                    null, pkg.getPackageName(), LIBRARY_TYPE_STATIC, true,
+                    pkg.getTargetSdkVersion(), usesLibraryInfos, availablePackages, newLibraries,
+                    null);
         }
         if (!pkg.getUsesOptionalLibraries().isEmpty()) {
             usesLibraryInfos = collectSharedLibraryInfos(pkg.getUsesOptionalLibraries(), null, null,
                     null, pkg.getPackageName(), "shared", false, pkg.getTargetSdkVersion(),
-                    usesLibraryInfos, availablePackages, newLibraries);
+                    usesLibraryInfos, availablePackages, newLibraries, null);
         }
         if (platformCompat.isChangeEnabledInternal(ENFORCE_NATIVE_SHARED_LIBRARY_DEPENDENCIES,
                 pkg.getPackageName(), pkg.getTargetSdkVersion())) {
@@ -945,13 +948,13 @@
                 usesLibraryInfos = collectSharedLibraryInfos(pkg.getUsesNativeLibraries(), null,
                         null, null, pkg.getPackageName(), "native shared", true,
                         pkg.getTargetSdkVersion(), usesLibraryInfos, availablePackages,
-                        newLibraries);
+                        newLibraries, null);
             }
             if (!pkg.getUsesOptionalNativeLibraries().isEmpty()) {
                 usesLibraryInfos = collectSharedLibraryInfos(pkg.getUsesOptionalNativeLibraries(),
                         null, null, null, pkg.getPackageName(), "native shared", false,
                         pkg.getTargetSdkVersion(), usesLibraryInfos, availablePackages,
-                        newLibraries);
+                        newLibraries, null);
             }
         }
         if (!pkg.getUsesSdkLibraries().isEmpty()) {
@@ -961,11 +964,34 @@
                     pkg.getUsesSdkLibrariesVersionsMajor(), pkg.getUsesSdkLibrariesCertDigests(),
                     pkg.getUsesSdkLibrariesOptional(),
                     pkg.getPackageName(), LIBRARY_TYPE_SDK, required, pkg.getTargetSdkVersion(),
-                    usesLibraryInfos, availablePackages, newLibraries);
+                    usesLibraryInfos, availablePackages, newLibraries, null);
         }
         return usesLibraryInfos;
     }
 
+    List<SharedLibraryInfo> collectMissingSharedLibraryInfos(PackageLite pkgLite)
+            throws PackageManagerException {
+        ArrayList<SharedLibraryInfo> missingSharedLibrary = new ArrayList<>();
+        synchronized (mPm.mLock) {
+            collectSharedLibraryInfos(pkgLite.getUsesSdkLibraries(),
+                    pkgLite.getUsesSdkLibrariesVersionsMajor(),
+                    pkgLite.getUsesSdkLibrariesCertDigests(),
+                    /*libsOptional=*/ null, pkgLite.getPackageName(), LIBRARY_TYPE_SDK,
+                    /*required=*/ true, pkgLite.getTargetSdk(),
+                    /*outUsedLibraries=*/ null, mPm.mPackages, /*newLibraries=*/ null,
+                    missingSharedLibrary);
+
+            collectSharedLibraryInfos(pkgLite.getUsesStaticLibraries(),
+                    pkgLite.getUsesStaticLibrariesVersions(),
+                    pkgLite.getUsesStaticLibrariesCertDigests(),
+                    /*libsOptional=*/ null, pkgLite.getPackageName(), LIBRARY_TYPE_STATIC,
+                    /*required=*/ true, pkgLite.getTargetSdk(),
+                    /*outUsedLibraries=*/ null, mPm.mPackages, /*newLibraries=*/ null,
+                    missingSharedLibrary);
+        }
+        return missingSharedLibrary;
+    }
+
     private ArrayList<SharedLibraryInfo> collectSharedLibraryInfos(
             @NonNull List<String> requestedLibraries,
             @Nullable long[] requiredVersions, @Nullable String[][] requiredCertDigests,
@@ -973,7 +999,8 @@
             @NonNull String packageName, @NonNull String libraryType, boolean required,
             int targetSdk, @Nullable ArrayList<SharedLibraryInfo> outUsedLibraries,
             @NonNull final Map<String, AndroidPackage> availablePackages,
-            @Nullable final Map<String, WatchedLongSparseArray<SharedLibraryInfo>> newLibraries)
+            @Nullable final Map<String, WatchedLongSparseArray<SharedLibraryInfo>> newLibraries,
+            @Nullable final List<SharedLibraryInfo> outMissingSharedLibraryInfos)
             throws PackageManagerException {
         final int libCount = requestedLibraries.size();
         for (int i = 0; i < libCount; i++) {
@@ -986,16 +1013,33 @@
                         libName, libVersion, mSharedLibraries, newLibraries);
             }
             if (libraryInfo == null) {
-                // Only allow app be installed if the app specifies the sdk-library dependency is
-                // optional
-                if (required || (LIBRARY_TYPE_SDK.equals(libraryType) && (libsOptional != null
-                        && !libsOptional[i]))) {
-                    throw new PackageManagerException(INSTALL_FAILED_MISSING_SHARED_LIBRARY,
-                            "Package " + packageName + " requires unavailable " + libraryType
-                                    + " library " + libName + "; failing!");
-                } else if (DEBUG_SHARED_LIBRARIES) {
-                    Slog.i(TAG, "Package " + packageName + " desires unavailable " + libraryType
-                            + " library " + libName + "; ignoring!");
+                if (required) {
+                    boolean isSdkOrStatic = libraryType.equals(LIBRARY_TYPE_SDK)
+                            || libraryType.equals(LIBRARY_TYPE_STATIC);
+                    if (isSdkOrStatic && outMissingSharedLibraryInfos != null) {
+                        // TODO(b/372862145): Pass the CertDigest too
+                        // If Dependency Installation is supported, try that instead of failing.
+                        SharedLibraryInfo missingLibrary = new SharedLibraryInfo(
+                                libName, libVersion, SharedLibraryInfo.TYPE_SDK_PACKAGE
+                        );
+                        outMissingSharedLibraryInfos.add(missingLibrary);
+                    } else {
+                        throw new PackageManagerException(INSTALL_FAILED_MISSING_SHARED_LIBRARY,
+                                "Package " + packageName + " requires unavailable " + libraryType
+                                + " library " + libName + "; failing!");
+                    }
+                } else {
+                    // Only allow app be installed if the app specifies the sdk-library
+                    // dependency is optional
+                    boolean isOptional = libsOptional != null && libsOptional[i];
+                    if (LIBRARY_TYPE_SDK.equals(libraryType) && !isOptional) {
+                        throw new PackageManagerException(INSTALL_FAILED_MISSING_SHARED_LIBRARY,
+                                "Package " + packageName + " requires unavailable " + libraryType
+                                + " library " + libName + "; failing!");
+                    } else if (DEBUG_SHARED_LIBRARIES) {
+                        Slog.i(TAG, "Package " + packageName + " desires unavailable " + libraryType
+                                + " library " + libName + "; ignoring!");
+                    }
                 }
             } else {
                 if (requiredVersions != null && requiredCertDigests != null) {
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
index d5bea4a..b3e68a35 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
@@ -19,6 +19,7 @@
 import static android.app.WallpaperManager.ORIENTATION_UNKNOWN;
 import static android.app.WallpaperManager.getOrientation;
 import static android.app.WallpaperManager.getRotatedOrientation;
+import static android.app.Flags.accurateWallpaperDownsampling;
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import static com.android.server.wallpaper.WallpaperUtils.RECORD_FILE;
@@ -378,7 +379,14 @@
         for (int i = 0; i < wallpaper.mCropHints.size(); i++) {
             Rect adjustedRect = new Rect(wallpaper.mCropHints.valueAt(i));
             adjustedRect.offset(-wallpaper.cropHint.left, -wallpaper.cropHint.top);
-            adjustedRect.scale(1f / wallpaper.mSampleSize);
+            if (accurateWallpaperDownsampling()) {
+                adjustedRect.left = (int) (0.5f + adjustedRect.left / wallpaper.mSampleSize);
+                adjustedRect.top = (int) (0.5f + adjustedRect.top / wallpaper.mSampleSize);
+                adjustedRect.right = (int) Math.floor(adjustedRect.right / wallpaper.mSampleSize);
+                adjustedRect.bottom = (int) Math.floor(adjustedRect.bottom / wallpaper.mSampleSize);
+            } else {
+                adjustedRect.scale(1f / wallpaper.mSampleSize);
+            }
             result.put(wallpaper.mCropHints.keyAt(i), adjustedRect);
         }
         return result;
@@ -603,6 +611,11 @@
                     float sampleSizeForThisOrientation = Math.max(1f, Math.min(
                             crop.width() / displayForThisOrientation.x,
                             crop.height() / displayForThisOrientation.y));
+                    if (accurateWallpaperDownsampling()) {
+                        sampleSizeForThisOrientation = Math.max(1f, Math.min(
+                                (float) crop.width() / displayForThisOrientation.x,
+                                (float) crop.height() / displayForThisOrientation.y));
+                    }
                     sampleSize = Math.min(sampleSize, sampleSizeForThisOrientation);
                 }
                 // If the total crop has more width or height than either the max texture size
@@ -746,8 +759,8 @@
                     final ImageDecoder.Source srcData =
                             ImageDecoder.createSource(wallpaper.getWallpaperFile());
                     final int finalScale = scale;
-                    final int rescaledBitmapWidth = (int) (0.5f + bitmapSize.x / sampleSize);
-                    final int rescaledBitmapHeight = (int) (0.5f + bitmapSize.y / sampleSize);
+                    final int rescaledBitmapWidth = (int) Math.ceil(bitmapSize.x / sampleSize);
+                    final int rescaledBitmapHeight = (int) Math.ceil(bitmapSize.y / sampleSize);
                     Bitmap cropped = ImageDecoder.decodeBitmap(srcData, (decoder, info, src) -> {
                         if (!multiCrop()) decoder.setTargetSampleSize(finalScale);
                         if (multiCrop()) {
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 10f096c..d019516 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -2380,8 +2380,8 @@
             SparseArray<Rect> relativeSuggestedCrops =
                     mWallpaperCropper.getRelativeCropHints(wallpaper);
             Point croppedBitmapSize = new Point(
-                    (int) (0.5f + wallpaper.cropHint.width() / wallpaper.mSampleSize),
-                    (int) (0.5f + wallpaper.cropHint.height() / wallpaper.mSampleSize));
+                    (int) Math.ceil(wallpaper.cropHint.width() / wallpaper.mSampleSize),
+                    (int) Math.ceil(wallpaper.cropHint.height() / wallpaper.mSampleSize));
             if (croppedBitmapSize.equals(0, 0)) {
                 // There is an ImageWallpaper, but there are no crop hints and the bitmap size is
                 // unknown (e.g. the default wallpaper). Return a special "null" value that will be
@@ -2410,6 +2410,27 @@
     }
 
     @Override
+    public Bundle getCurrentBitmapCrops(int which, int userId) {
+        userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(),
+                Binder.getCallingUid(), userId, false, true, "getBitmapCrop", null);
+        synchronized (mLock) {
+            checkPermission(READ_WALLPAPER_INTERNAL);
+            WallpaperData wallpaper = (which == FLAG_LOCK) ? mLockWallpaperMap.get(userId)
+                    : mWallpaperMap.get(userId);
+            if (wallpaper == null || !mImageWallpaper.equals(wallpaper.getComponent())) {
+                return null;
+            }
+            Bundle bundle = new Bundle();
+            for (int i = 0; i < wallpaper.mCropHints.size(); i++) {
+                String key = String.valueOf(wallpaper.mCropHints.keyAt(i));
+                Rect rect = wallpaper.mCropHints.valueAt(i);
+                bundle.putParcelable(key, rect);
+            }
+            return bundle;
+        }
+    }
+
+    @Override
     public List<Rect> getFutureBitmapCrops(Point bitmapSize, List<Point> displaySizes,
             int[] screenOrientations, List<Rect> crops) {
         SparseArray<Rect> cropMap = getCropMap(screenOrientations, crops);
diff --git a/services/core/java/com/android/server/wm/SurfaceAnimator.java b/services/core/java/com/android/server/wm/SurfaceAnimator.java
index 3f6e915..9a48d5b 100644
--- a/services/core/java/com/android/server/wm/SurfaceAnimator.java
+++ b/services/core/java/com/android/server/wm/SurfaceAnimator.java
@@ -18,7 +18,6 @@
 
 import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_ANIM;
 import static com.android.server.wm.SurfaceAnimatorProto.ANIMATION_ADAPTER;
-import static com.android.server.wm.SurfaceAnimatorProto.ANIMATION_START_DELAYED;
 import static com.android.server.wm.SurfaceAnimatorProto.LEASH;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
@@ -90,8 +89,6 @@
     @Nullable
     private Runnable mAnimationCancelledCallback;
 
-    private boolean mAnimationStartDelayed;
-
     private boolean mAnimationFinished;
 
     /**
@@ -188,10 +185,6 @@
             mAnimatable.onAnimationLeashCreated(t, mLeash);
         }
         mAnimatable.onLeashAnimationStarting(t, mLeash);
-        if (mAnimationStartDelayed) {
-            ProtoLog.i(WM_DEBUG_ANIM, "Animation start delayed for %s", mAnimatable);
-            return;
-        }
         mAnimation.startAnimation(mLeash, t, type, mInnerAnimationFinishedCallback);
         if (ProtoLog.isEnabled(WM_DEBUG_ANIM, LogLevel.DEBUG)) {
             StringWriter sw = new StringWriter();
@@ -215,36 +208,7 @@
                 null /* animationCancelledCallback */, null /* snapshotAnim */, null /* freezer */);
     }
 
-    /**
-     * Begins with delaying all animations to start. Any subsequent call to {@link #startAnimation}
-     * will not start the animation until {@link #endDelayingAnimationStart} is called. When an
-     * animation start is being delayed, the animator is considered animating already.
-     */
-    void startDelayingAnimationStart() {
-
-        // We only allow delaying animation start we are not currently animating
-        if (!isAnimating()) {
-            mAnimationStartDelayed = true;
-        }
-    }
-
-    /**
-     * See {@link #startDelayingAnimationStart}.
-     */
-    void endDelayingAnimationStart() {
-        final boolean delayed = mAnimationStartDelayed;
-        mAnimationStartDelayed = false;
-        if (delayed && mAnimation != null) {
-            mAnimation.startAnimation(mLeash, mAnimatable.getSyncTransaction(),
-                    mAnimationType, mInnerAnimationFinishedCallback);
-            mAnimatable.commitPendingTransaction();
-        }
-    }
-
-    /**
-     * @return Whether we are currently running an animation, or we have a pending animation that
-     *         is waiting to be started with {@link #endDelayingAnimationStart}
-     */
+    /** Returns whether it is currently running an animation. */
     boolean isAnimating() {
         return mAnimation != null;
     }
@@ -290,15 +254,6 @@
     }
 
     /**
-     * Reparents the surface.
-     *
-     * @see #setLayer
-     */
-    void reparent(Transaction t, SurfaceControl newParent) {
-        t.reparent(mLeash != null ? mLeash : mAnimatable.getSurfaceControl(), newParent);
-    }
-
-    /**
      * @return True if the surface is attached to the leash; false otherwise.
      */
     boolean hasLeash() {
@@ -319,7 +274,6 @@
             Slog.w(TAG, "Unable to transfer animation, because " + from + " animation is finished");
             return;
         }
-        endDelayingAnimationStart();
         final Transaction t = mAnimatable.getSyncTransaction();
         cancelAnimation(t, true /* restarting */, true /* forwardCancel */);
         mLeash = from.mLeash;
@@ -336,10 +290,6 @@
         mService.mAnimationTransferMap.put(mAnimation, this);
     }
 
-    boolean isAnimationStartDelayed() {
-        return mAnimationStartDelayed;
-    }
-
     /**
      * Cancels the animation, and resets the leash.
      *
@@ -361,7 +311,7 @@
         final SurfaceFreezer.Snapshot snapshot = mSnapshot;
         reset(t, false);
         if (animation != null) {
-            if (!mAnimationStartDelayed && forwardCancel) {
+            if (forwardCancel) {
                 animation.onAnimationCancelled(leash);
                 if (animationCancelledCallback != null) {
                     animationCancelledCallback.run();
@@ -386,10 +336,6 @@
                 mService.scheduleAnimationLocked();
             }
         }
-
-        if (!restarting) {
-            mAnimationStartDelayed = false;
-        }
     }
 
     private void reset(Transaction t, boolean destroyLeash) {
@@ -495,14 +441,12 @@
         if (mLeash != null) {
             mLeash.dumpDebug(proto, LEASH);
         }
-        proto.write(ANIMATION_START_DELAYED, mAnimationStartDelayed);
         proto.end(token);
     }
 
     void dump(PrintWriter pw, String prefix) {
         pw.print(prefix); pw.print("mLeash="); pw.print(mLeash);
-        pw.print(" mAnimationType=" + animationTypeToString(mAnimationType));
-        pw.println(mAnimationStartDelayed ? " mAnimationStartDelayed=true" : "");
+        pw.print(" mAnimationType="); pw.println(animationTypeToString(mAnimationType));
         pw.print(prefix); pw.print("Animation: "); pw.println(mAnimation);
         if (mAnimation != null) {
             mAnimation.dump(pw, prefix + "  ");
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index e0c473d..5f92bb6 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -3215,8 +3215,7 @@
         final boolean isChanging = AppTransition.isChangeTransitOld(transit) && enter
                 && isChangingAppTransition();
 
-        // Delaying animation start isn't compatible with remote animations at all.
-        if (controller != null && !mSurfaceAnimator.isAnimationStartDelayed()) {
+        if (controller != null) {
             // Here we load App XML in order to read com.android.R.styleable#Animation_showBackdrop.
             boolean showBackdrop = false;
             // Optionally set backdrop color if App explicitly provides it through
@@ -3639,20 +3638,6 @@
         return getAnimatingContainer(PARENTS, ANIMATION_TYPE_ALL);
     }
 
-    /**
-     * @see SurfaceAnimator#startDelayingAnimationStart
-     */
-    void startDelayingAnimationStart() {
-        mSurfaceAnimator.startDelayingAnimationStart();
-    }
-
-    /**
-     * @see SurfaceAnimator#endDelayingAnimationStart
-     */
-    void endDelayingAnimationStart() {
-        mSurfaceAnimator.endDelayingAnimationStart();
-    }
-
     @Override
     public int getSurfaceWidth() {
         return mSurfaceControl.getWidth();
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java
index cc5573b..f34ec72 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java
@@ -19,6 +19,7 @@
 import static android.hardware.devicestate.DeviceState.PROPERTY_EMULATED_ONLY;
 import static android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_DUAL_DISPLAY_INTERNAL_DEFAULT;
 import static android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_REAR_DISPLAY;
+import static android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT;
 import static android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_INNER_PRIMARY;
 import static android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY;
 import static android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED;
@@ -71,6 +72,7 @@
     private static final int DEVICE_STATE_OPENED = 2;
     private static final int DEVICE_STATE_REAR_DISPLAY = 3;
     private static final int DEVICE_STATE_CONCURRENT_INNER_DEFAULT = 4;
+    private static final int DEVICE_STATE_REAR_DISPLAY_OUTER_DEFAULT = 5;
     private static final int TENT_MODE_SWITCH_ANGLE_DEGREES = 90;
     private static final int TABLE_TOP_MODE_SWITCH_ANGLE_DEGREES = 125;
     private static final int MIN_CLOSED_ANGLE_DEGREES = 0;
@@ -130,14 +132,17 @@
                             return hingeAngle >= MAX_CLOSED_ANGLE_DEGREES
                                     && hingeAngle <= TABLE_TOP_MODE_SWITCH_ANGLE_DEGREES;
                         }),
-                createConfig(getOpenedDeviceState(), /* activeStatePredicate= */
-                        ALLOWED),
-                createConfig(getRearDisplayDeviceState(), /* activeStatePredicate= */
-                        NOT_ALLOWED),
-                createConfig(getDualDisplayDeviceState(), /* activeStatePredicate= */
-                        NOT_ALLOWED, /* availabilityPredicate= */
-                        provider -> !mIsDualDisplayBlockingEnabled
-                                || provider.hasNoConnectedExternalDisplay())};
+                createConfig(getOpenedDeviceState(),
+                        /* activeStatePredicate= */ ALLOWED),
+                createConfig(getRearDisplayDeviceState(),
+                        /* activeStatePredicate= */ NOT_ALLOWED),
+                createConfig(getDualDisplayDeviceState(),
+                        /* activeStatePredicate= */ NOT_ALLOWED,
+                        /* availabilityPredicate= */ provider -> !mIsDualDisplayBlockingEnabled
+                                || provider.hasNoConnectedExternalDisplay()),
+                createConfig(getRearDisplayOuterDefaultState(),
+                        /* activeStatePredicate= */ NOT_ALLOWED)
+        };
     }
 
     private DeviceStatePredicateWrapper createClosedConfiguration(
@@ -266,4 +271,24 @@
                 .setSystemProperties(systemProperties)
                 .build());
     }
+
+    /**
+     * Returns the {link DeviceState.Configuration} that represents the new rear display state
+     * where the inner display is also enabled, showing a system affordance to exit the state.
+     */
+    @NonNull
+    private DeviceState getRearDisplayOuterDefaultState() {
+        Set<@DeviceState.SystemDeviceStateProperties Integer> systemProperties = new HashSet<>(
+                List.of(PROPERTY_EMULATED_ONLY,
+                        PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY,
+                        PROPERTY_POLICY_AVAILABLE_FOR_APP_REQUEST,
+                        PROPERTY_FEATURE_REAR_DISPLAY,
+                        PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT));
+
+        return new DeviceState(new DeviceState.Configuration.Builder(
+                DEVICE_STATE_REAR_DISPLAY_OUTER_DEFAULT,
+                "REAR_DISPLAY_OUTER_DEFAULT")
+                .setSystemProperties(systemProperties)
+                .build());
+    }
 }
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 9759772..19b0343 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -1621,7 +1621,8 @@
             mSystemServiceManager.startService(ROLE_SERVICE_CLASS);
             t.traceEnd();
 
-            if (!isWatch && android.app.supervision.flags.Flags.supervisionApi()) {
+            if (android.app.supervision.flags.Flags.supervisionApi()
+                    && (!isWatch || android.app.supervision.flags.Flags.supervisionApiOnWear())) {
                 t.traceBegin("StartSupervisionService");
                 mSystemServiceManager.startService(SupervisionService.Lifecycle.class);
                 t.traceEnd();
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/BroadcastHelperTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/BroadcastHelperTest.java
new file mode 100644
index 0000000..1be5cef
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/BroadcastHelperTest.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pm;
+
+import static android.content.pm.Flags.FLAG_REDUCE_BROADCASTS_FOR_COMPONENT_STATE_CHANGES;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.Message;
+import android.os.UserHandle;
+import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.AppModeNonSdkSandbox;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.internal.pm.parsing.pkg.AndroidPackageInternal;
+import com.android.internal.pm.pkg.component.ParsedActivity;
+import com.android.server.pm.pkg.PackageStateInternal;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+
+@AppModeFull
+@AppModeNonSdkSandbox
+@RunWith(AndroidJUnit4.class)
+public class BroadcastHelperTest {
+    private static final String TAG = "BroadcastHelperTest";
+    private static final String PACKAGE_CHANGED_TEST_PACKAGE_NAME = "testpackagename";
+    private static final String PACKAGE_CHANGED_TEST_MAIN_ACTIVITY =
+            PACKAGE_CHANGED_TEST_PACKAGE_NAME + ".MainActivity";
+    private static final String PERMISSION_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED =
+            "android.permission.INTERNAL_RECEIVE_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED";
+
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    @Mock
+    ActivityManagerInternal mMockActivityManagerInternal;
+    @Mock
+    AndroidPackageInternal mMockAndroidPackageInternal;
+    @Mock
+    Computer mMockSnapshot;
+    @Mock
+    Handler mMockHandler;
+    @Mock
+    PackageManagerServiceInjector mMockPackageManagerServiceInjector;
+    @Mock
+    PackageMonitorCallbackHelper mMockPackageMonitorCallbackHelper;
+    @Mock
+    PackageStateInternal mMockPackageStateInternal;
+    @Mock
+    ParsedActivity mMockParsedActivity;
+    @Mock
+    UserManagerInternal mMockUserManagerInternal;
+
+    private Context mContext;
+    private BroadcastHelper mBroadcastHelper;
+
+    @Before
+    public void setup() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+
+        when(mMockHandler.sendMessageAtTime(any(Message.class), anyLong())).thenAnswer(
+                i -> {
+                    ((Message) i.getArguments()[0]).getCallback().run();
+                    return true;
+                });
+        when(mMockPackageManagerServiceInjector.getActivityManagerInternal()).thenReturn(
+                mMockActivityManagerInternal);
+        when(mMockPackageManagerServiceInjector.getContext()).thenReturn(mContext);
+        when(mMockPackageManagerServiceInjector.getHandler()).thenReturn(mMockHandler);
+        when(mMockPackageManagerServiceInjector.getPackageMonitorCallbackHelper()).thenReturn(
+                mMockPackageMonitorCallbackHelper);
+        when(mMockPackageManagerServiceInjector.getUserManagerInternal()).thenReturn(
+                mMockUserManagerInternal);
+
+        mBroadcastHelper = new BroadcastHelper(mMockPackageManagerServiceInjector);
+    }
+
+    @RequiresFlagsEnabled(FLAG_REDUCE_BROADCASTS_FOR_COMPONENT_STATE_CHANGES)
+    @Test
+    public void changeNonExportedComponent_sendPackageChangedBroadcastToSystem_withPermission()
+            throws Exception {
+        changeComponentAndSendPackageChangedBroadcast(false /* changeExportedComponent */);
+
+        ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
+        verify(mMockActivityManagerInternal).broadcastIntentWithCallback(
+                captor.capture(), eq(null),
+                eq(new String[]{PERMISSION_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED}),
+                anyInt(), eq(null), eq(null), eq(null));
+        Intent intent = captor.getValue();
+        assertNotNull(intent);
+        assertThat(intent.getPackage()).isEqualTo("android");
+    }
+
+    @RequiresFlagsEnabled(FLAG_REDUCE_BROADCASTS_FOR_COMPONENT_STATE_CHANGES)
+    @Test
+    public void changeNonExportedComponent_sendPackageChangedBroadcastToApplicationItself()
+            throws Exception {
+        changeComponentAndSendPackageChangedBroadcast(false /* changeExportedComponent */);
+
+        ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
+        verify(mMockActivityManagerInternal).broadcastIntentWithCallback(captor.capture(), eq(null),
+                eq(null), anyInt(), eq(null), eq(null), eq(null));
+        Intent intent = captor.getValue();
+        assertNotNull(intent);
+        assertThat(intent.getPackage()).isEqualTo(PACKAGE_CHANGED_TEST_PACKAGE_NAME);
+    }
+
+    @Test
+    public void changeExportedComponent_sendPackageChangedBroadcastToAll() throws Exception {
+        changeComponentAndSendPackageChangedBroadcast(true /* changeExportedComponent */);
+
+        ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
+        verify(mMockActivityManagerInternal).broadcastIntentWithCallback(captor.capture(), eq(null),
+                eq(null), anyInt(), eq(null), eq(null), eq(null));
+        Intent intent = captor.getValue();
+        assertNotNull(intent);
+        assertNull(intent.getPackage());
+    }
+
+    private void changeComponentAndSendPackageChangedBroadcast(boolean changeExportedComponent) {
+        when(mMockSnapshot.getPackageStateInternal(eq(PACKAGE_CHANGED_TEST_PACKAGE_NAME),
+                anyInt())).thenReturn(mMockPackageStateInternal);
+        when(mMockSnapshot.isInstantAppInternal(any(), anyInt(), anyInt())).thenReturn(false);
+        when(mMockSnapshot.getVisibilityAllowLists(any(), any())).thenReturn(null);
+        when(mMockPackageStateInternal.getPkg()).thenReturn(mMockAndroidPackageInternal);
+
+        when(mMockParsedActivity.getClassName()).thenReturn(
+                PACKAGE_CHANGED_TEST_MAIN_ACTIVITY);
+        when(mMockParsedActivity.isExported()).thenReturn(changeExportedComponent);
+        ArrayList<ParsedActivity> parsedActivities = new ArrayList<>();
+        parsedActivities.add(mMockParsedActivity);
+
+        when(mMockAndroidPackageInternal.getReceivers()).thenReturn(new ArrayList<>());
+        when(mMockAndroidPackageInternal.getProviders()).thenReturn(new ArrayList<>());
+        when(mMockAndroidPackageInternal.getServices()).thenReturn(new ArrayList<>());
+        when(mMockAndroidPackageInternal.getActivities()).thenReturn(parsedActivities);
+
+        doNothing().when(mMockPackageMonitorCallbackHelper).notifyPackageChanged(any(),
+                anyBoolean(), any(), anyInt(), any(), any(), any(), any(), any());
+        when(mMockActivityManagerInternal.broadcastIntentWithCallback(any(), any(), any(), anyInt(),
+                any(), any(), any())).thenReturn(ActivityManager.BROADCAST_SUCCESS);
+
+        ArrayList<String> componentNames = new ArrayList<>();
+        componentNames.add(PACKAGE_CHANGED_TEST_MAIN_ACTIVITY);
+
+        mBroadcastHelper.sendPackageChangedBroadcast(mMockSnapshot,
+                PACKAGE_CHANGED_TEST_PACKAGE_NAME, true /* dontKillApp */, componentNames,
+                UserHandle.USER_SYSTEM, "test" /* reason */);
+    }
+}
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt
index 09d0e4a..5a59c57 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt
@@ -201,7 +201,8 @@
             /* preVerifiedDomains */ DomainSet(setOf("com.foo", "com.bar")),
             /* VerifierController */ mock(VerifierController::class.java),
             /* initialVerificationPolicy */ VERIFICATION_POLICY_BLOCK_FAIL_OPEN,
-            /* currentVerificationPolicy */ VERIFICATION_POLICY_BLOCK_FAIL_CLOSED
+            /* currentVerificationPolicy */ VERIFICATION_POLICY_BLOCK_FAIL_CLOSED,
+            /* installDependencyHelper */ null
         )
     }
 
@@ -256,7 +257,8 @@
                                 mTmpDir,
                                 mock(PackageSessionProvider::class.java),
                                 mock(SilentUpdatePolicy::class.java),
-                                mock(VerifierController::class.java)
+                                mock(VerifierController::class.java),
+                                mock(InstallDependencyHelper::class.java)
                             )
                             ret.add(session)
                         } catch (e: Exception) {
diff --git a/services/tests/mockingservicestests/Android.bp b/services/tests/mockingservicestests/Android.bp
index be698b2..6acf242 100644
--- a/services/tests/mockingservicestests/Android.bp
+++ b/services/tests/mockingservicestests/Android.bp
@@ -109,6 +109,10 @@
     optimize: {
         enabled: false,
     },
+
+    data: [
+        ":HelloWorldUsingSdk1And2",
+    ],
 }
 
 java_library {
@@ -134,6 +138,7 @@
         "androidx.annotation_annotation",
         "androidx.test.rules",
         "services.core",
+        "servicestests-utils-mockito-extended",
     ],
     srcs: [
         "src/com/android/server/am/BroadcastRecordTest.java",
diff --git a/services/tests/mockingservicestests/AndroidTest.xml b/services/tests/mockingservicestests/AndroidTest.xml
index 7782d57..2b90119 100644
--- a/services/tests/mockingservicestests/AndroidTest.xml
+++ b/services/tests/mockingservicestests/AndroidTest.xml
@@ -23,6 +23,12 @@
         <option name="test-file-name" value="FrameworksMockingServicesTests.apk" />
     </target_preparer>
 
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="cleanup" value="true"/>
+        <option name="push-file" key="HelloWorldUsingSdk1And2.apk"
+            value="/data/local/tmp/tests/smockingservicestest/pm/HelloWorldUsingSdk1And2.apk"/>
+    </target_preparer>
+
     <option name="test-tag" value="FrameworksMockingServicesTests" />
 
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
index d602660..a1a8b0e 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
@@ -41,6 +41,7 @@
 import android.os.UserHandle;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.Settings;
 import android.util.SparseArray;
 
@@ -97,6 +98,9 @@
             .spyStatic(ProcessList.class)
             .build();
 
+
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
     @Rule
     public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
 
@@ -112,6 +116,8 @@
     AlarmManagerInternal mAlarmManagerInt;
     @Mock
     ProcessList mProcessList;
+    @Mock
+    PlatformCompat mPlatformCompat;
 
     @Mock
     AppStartInfoTracker mAppStartInfoTracker;
@@ -178,6 +184,11 @@
         doReturn(false).when(mSkipPolicy).disallowBackgroundStart(any());
 
         doReturn(mAppStartInfoTracker).when(mProcessList).getAppStartInfoTracker();
+
+        doReturn(true).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastFilter.CHANGE_RESTRICT_PRIORITY_VALUES), anyInt());
+        doReturn(true).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), anyInt());
     }
 
     public void tearDown() throws Exception {
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
index 100b548..1481085 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
@@ -18,6 +18,7 @@
 
 import static android.app.ActivityManager.PROCESS_STATE_UNKNOWN;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.internal.util.FrameworkStatsLog.BROADCAST_DELIVERY_EVENT_REPORTED;
 import static com.android.internal.util.FrameworkStatsLog.BROADCAST_DELIVERY_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_COLD;
@@ -49,7 +50,6 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
@@ -65,7 +65,6 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
-import android.content.pm.ResolveInfo;
 import android.media.AudioManager;
 import android.os.Bundle;
 import android.os.BundleMerger;
@@ -73,6 +72,8 @@
 import android.os.Process;
 import android.os.SystemClock;
 import android.os.UserHandle;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
 import android.util.IndentingPrintWriter;
 import android.util.Pair;
 
@@ -182,10 +183,6 @@
         return mock(Intent.class);
     }
 
-    private static ResolveInfo makeMockManifestReceiver() {
-        return mock(ResolveInfo.class);
-    }
-
     private static BroadcastFilter makeMockRegisteredReceiver() {
         return mock(BroadcastFilter.class);
     }
@@ -214,7 +211,8 @@
         return new BroadcastRecord(mImpl, intent, mProcess, PACKAGE_RED, null, 21, TEST_UID, false,
                 null, null, null, null, AppOpsManager.OP_NONE, options, receivers, null, resultTo,
                 Activity.RESULT_OK, null, null, ordered, false, false, UserHandle.USER_SYSTEM,
-                BackgroundStartPrivileges.NONE, false, null, PROCESS_STATE_UNKNOWN);
+                BackgroundStartPrivileges.NONE, false, null, PROCESS_STATE_UNKNOWN,
+                mPlatformCompat);
     }
 
     private void enqueueOrReplaceBroadcast(BroadcastProcessQueue queue,
@@ -646,7 +644,8 @@
     @Test
     public void testRunnableAt_Cached_Manifest() {
         doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), null,
-                List.of(makeMockManifestReceiver()), null, false), REASON_CONTAINS_MANIFEST);
+                List.of(makeManifestReceiver(PACKAGE_RED, CLASS_RED)), null, false),
+                REASON_CONTAINS_MANIFEST);
     }
 
     @Test
@@ -679,6 +678,19 @@
                 List.of(makeMockRegisteredReceiver()), null, false), REASON_CONTAINS_ALARM);
     }
 
+    @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @Test
+    public void testRunnableAt_Cached_Prioritized_NonDeferrable_flagDisabled() {
+        final List receivers = List.of(
+                withPriority(makeManifestReceiver(PACKAGE_RED, PACKAGE_RED), 10),
+                withPriority(makeManifestReceiver(PACKAGE_GREEN, PACKAGE_GREEN), -10));
+        final BroadcastOptions options = BroadcastOptions.makeBasic()
+                .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_NONE);
+        doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), options,
+                receivers, null, false), REASON_CONTAINS_PRIORITIZED);
+    }
+
+    @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
     @Test
     public void testRunnableAt_Cached_Prioritized_NonDeferrable() {
         final List receivers = List.of(
@@ -687,6 +699,32 @@
         final BroadcastOptions options = BroadcastOptions.makeBasic()
                 .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_NONE);
         doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), options,
+                receivers, null, false), REASON_CONTAINS_MANIFEST);
+    }
+
+    @Test
+    public void testRunnableAt_Cached_Ordered_NonDeferrable() {
+        final List receivers = List.of(
+                withPriority(makeManifestReceiver(PACKAGE_RED, PACKAGE_RED), 10),
+                withPriority(makeManifestReceiver(PACKAGE_GREEN, PACKAGE_GREEN), -10));
+        final BroadcastOptions options = BroadcastOptions.makeBasic()
+                .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_NONE);
+        doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), options,
+                receivers, mock(IIntentReceiver.class), true), REASON_CONTAINS_ORDERED);
+    }
+
+    @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @Test
+    public void testRunnableAt_Cached_Prioritized_NonDeferrable_changeIdDisabled() {
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE),
+                eq(getUidForPackage(PACKAGE_GREEN)));
+        final List receivers = List.of(
+                withPriority(makeManifestReceiver(PACKAGE_RED, PACKAGE_RED), 10),
+                withPriority(makeManifestReceiver(PACKAGE_GREEN, PACKAGE_GREEN), -10));
+        final BroadcastOptions options = BroadcastOptions.makeBasic()
+                .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_NONE);
+        doRunnableAt_Cached(makeBroadcastRecord(makeMockIntent(), options,
                 receivers, null, false), REASON_CONTAINS_PRIORITIZED);
     }
 
@@ -1136,6 +1174,63 @@
         verifyPendingRecords(blueQueue, List.of(screenOn));
     }
 
+    @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @SuppressWarnings("GuardedBy")
+    @Test
+    public void testDeliveryGroupPolicy_prioritized_diffReceivers_flagDisabled() {
+        final Intent screenOn = new Intent(Intent.ACTION_SCREEN_ON);
+        final Intent screenOff = new Intent(Intent.ACTION_SCREEN_OFF);
+        final BroadcastOptions screenOnOffOptions = BroadcastOptions.makeBasic()
+                .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT)
+                .setDeliveryGroupMatchingKey("screenOnOff", Intent.ACTION_SCREEN_ON);
+
+        final Object greenReceiver = withPriority(
+                makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 10);
+        final Object redReceiver = withPriority(
+                makeManifestReceiver(PACKAGE_RED, CLASS_RED), 5);
+        final Object blueReceiver = withPriority(
+                makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE), 0);
+
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions,
+                List.of(greenReceiver, blueReceiver), false));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOff, screenOnOffOptions,
+                List.of(greenReceiver, redReceiver, blueReceiver), false));
+        final BroadcastProcessQueue greenQueue = mImpl.getProcessQueue(PACKAGE_GREEN,
+                getUidForPackage(PACKAGE_GREEN));
+        final BroadcastProcessQueue redQueue = mImpl.getProcessQueue(PACKAGE_RED,
+                getUidForPackage(PACKAGE_RED));
+        final BroadcastProcessQueue blueQueue = mImpl.getProcessQueue(PACKAGE_BLUE,
+                getUidForPackage(PACKAGE_BLUE));
+        verifyPendingRecords(greenQueue, List.of(screenOff));
+        verifyPendingRecords(redQueue, List.of(screenOff));
+        verifyPendingRecords(blueQueue, List.of(screenOff));
+
+        assertTrue(greenQueue.isEmpty());
+        assertTrue(redQueue.isEmpty());
+        assertTrue(blueQueue.isEmpty());
+
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOff, screenOnOffOptions,
+                List.of(greenReceiver, redReceiver, blueReceiver), false));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions,
+                List.of(greenReceiver, blueReceiver), false));
+        verifyPendingRecords(greenQueue, List.of(screenOff, screenOn));
+        verifyPendingRecords(redQueue, List.of(screenOff));
+        verifyPendingRecords(blueQueue, List.of(screenOff, screenOn));
+
+        final BroadcastRecord screenOffRecord = makeBroadcastRecord(screenOff, screenOnOffOptions,
+                List.of(greenReceiver, redReceiver, blueReceiver), false);
+        screenOffRecord.setDeliveryState(2, BroadcastRecord.DELIVERY_DEFERRED,
+                "testDeliveryGroupPolicy_prioritized_diffReceivers_flagDisabled");
+        mImpl.enqueueBroadcastLocked(screenOffRecord);
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions,
+                List.of(greenReceiver, blueReceiver), false));
+        verifyPendingRecords(greenQueue, List.of(screenOff, screenOn));
+        verifyPendingRecords(redQueue, List.of(screenOff));
+        verifyPendingRecords(blueQueue, List.of(screenOn));
+    }
+
+    @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @SuppressWarnings("GuardedBy")
     @Test
     public void testDeliveryGroupPolicy_prioritized_diffReceivers() {
         final Intent screenOn = new Intent(Intent.ACTION_SCREEN_ON);
@@ -1173,6 +1268,65 @@
                 List.of(greenReceiver, redReceiver, blueReceiver), false));
         mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions,
                 List.of(greenReceiver, blueReceiver), false));
+        verifyPendingRecords(greenQueue, List.of(screenOn));
+        verifyPendingRecords(redQueue, List.of(screenOff));
+        verifyPendingRecords(blueQueue, List.of(screenOn));
+
+        final BroadcastRecord screenOffRecord = makeBroadcastRecord(screenOff, screenOnOffOptions,
+                List.of(greenReceiver, redReceiver, blueReceiver), false);
+        screenOffRecord.setDeliveryState(2, BroadcastRecord.DELIVERY_DEFERRED,
+                "testDeliveryGroupPolicy_prioritized_diffReceivers");
+        mImpl.enqueueBroadcastLocked(screenOffRecord);
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions,
+                List.of(greenReceiver, blueReceiver), false));
+        verifyPendingRecords(greenQueue, List.of(screenOn));
+        verifyPendingRecords(redQueue, List.of(screenOff));
+        verifyPendingRecords(blueQueue, List.of(screenOn));
+    }
+
+    @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @SuppressWarnings("GuardedBy")
+    @Test
+    public void testDeliveryGroupPolicy_prioritized_diffReceivers_changeIdDisabled() {
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE),
+                eq(getUidForPackage(PACKAGE_GREEN)));
+
+        final Intent screenOn = new Intent(Intent.ACTION_SCREEN_ON);
+        final Intent screenOff = new Intent(Intent.ACTION_SCREEN_OFF);
+        final BroadcastOptions screenOnOffOptions = BroadcastOptions.makeBasic()
+                .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT)
+                .setDeliveryGroupMatchingKey("screenOnOff", Intent.ACTION_SCREEN_ON);
+
+        final Object greenReceiver = withPriority(
+                makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 10);
+        final Object redReceiver = withPriority(
+                makeManifestReceiver(PACKAGE_RED, CLASS_RED), 5);
+        final Object blueReceiver = withPriority(
+                makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE), 0);
+
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions,
+                List.of(greenReceiver, blueReceiver), false));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOff, screenOnOffOptions,
+                List.of(greenReceiver, redReceiver, blueReceiver), false));
+        final BroadcastProcessQueue greenQueue = mImpl.getProcessQueue(PACKAGE_GREEN,
+                getUidForPackage(PACKAGE_GREEN));
+        final BroadcastProcessQueue redQueue = mImpl.getProcessQueue(PACKAGE_RED,
+                getUidForPackage(PACKAGE_RED));
+        final BroadcastProcessQueue blueQueue = mImpl.getProcessQueue(PACKAGE_BLUE,
+                getUidForPackage(PACKAGE_BLUE));
+        verifyPendingRecords(greenQueue, List.of(screenOff));
+        verifyPendingRecords(redQueue, List.of(screenOff));
+        verifyPendingRecords(blueQueue, List.of(screenOff));
+
+        assertTrue(greenQueue.isEmpty());
+        assertTrue(redQueue.isEmpty());
+        assertTrue(blueQueue.isEmpty());
+
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOff, screenOnOffOptions,
+                List.of(greenReceiver, redReceiver, blueReceiver), false));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, screenOnOffOptions,
+                List.of(greenReceiver, blueReceiver), false));
         verifyPendingRecords(greenQueue, List.of(screenOff, screenOn));
         verifyPendingRecords(redQueue, List.of(screenOff));
         verifyPendingRecords(blueQueue, List.of(screenOff, screenOn));
@@ -1569,8 +1723,9 @@
         verifyPendingRecords(redQueue, List.of(userPresent, timeTick));
     }
 
+    @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
     @Test
-    public void testDeliveryDeferredForCached() throws Exception {
+    public void testDeliveryDeferredForCached_flagDisabled() throws Exception {
         final ProcessRecord greenProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
         final ProcessRecord redProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_RED));
 
@@ -1664,8 +1819,217 @@
         }, false /* andRemove */);
     }
 
+    @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @SuppressWarnings("GuardedBy")
     @Test
-    public void testDeliveryDeferredForCached_withInfiniteDeferred() throws Exception {
+    public void testDeliveryDeferredForCached_changeIdDisabled() throws Exception {
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE),
+                eq(getUidForPackage(PACKAGE_GREEN)));
+
+        final ProcessRecord greenProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+        final ProcessRecord redProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_RED));
+
+        final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK);
+        final BroadcastRecord timeTickRecord = makeBroadcastRecord(timeTick,
+                List.of(makeRegisteredReceiver(greenProcess, 0)));
+
+        final Intent batteryChanged = new Intent(Intent.ACTION_BATTERY_CHANGED);
+        final BroadcastOptions optionsBatteryChanged =
+                BroadcastOptions.makeWithDeferUntilActive(true);
+        final BroadcastRecord batteryChangedRecord = makeBroadcastRecord(batteryChanged,
+                optionsBatteryChanged,
+                List.of(makeRegisteredReceiver(greenProcess, 10),
+                        makeRegisteredReceiver(redProcess, 0)),
+                false /* ordered */);
+
+        mImpl.enqueueBroadcastLocked(timeTickRecord);
+        mImpl.enqueueBroadcastLocked(batteryChangedRecord);
+
+        final BroadcastProcessQueue greenQueue = mImpl.getProcessQueue(PACKAGE_GREEN,
+                getUidForPackage(PACKAGE_GREEN));
+        final BroadcastProcessQueue redQueue = mImpl.getProcessQueue(PACKAGE_RED,
+                getUidForPackage(PACKAGE_RED));
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason());
+        assertFalse(greenQueue.shouldBeDeferred());
+        assertEquals(BroadcastProcessQueue.REASON_BLOCKED, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        // Simulate process state change
+        greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */,
+                true /* processFreezable */);
+        greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply,
+                mImpl.mBroadcastConsumerDeferClear);
+
+        assertEquals(BroadcastProcessQueue.REASON_CACHED, greenQueue.getRunnableAtReason());
+        assertTrue(greenQueue.shouldBeDeferred());
+        // Once the broadcasts to green process are deferred, broadcasts to red process
+        // shouldn't be blocked anymore.
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        // All broadcasts to green process should be deferred.
+        greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i));
+        }, false /* andRemove */);
+        redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+
+        final Intent packageChanged = new Intent(Intent.ACTION_PACKAGE_CHANGED);
+        final BroadcastRecord packageChangedRecord = makeBroadcastRecord(packageChanged,
+                List.of(makeRegisteredReceiver(greenProcess, 0)));
+        mImpl.enqueueBroadcastLocked(packageChangedRecord);
+
+        assertEquals(BroadcastProcessQueue.REASON_CACHED, greenQueue.getRunnableAtReason());
+        assertTrue(greenQueue.shouldBeDeferred());
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        // All broadcasts to the green process, including the newly enqueued one, should be
+        // deferred.
+        greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i));
+        }, false /* andRemove */);
+        redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+
+        // Simulate process state change
+        greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */,
+                false /* processFreezable */);
+        greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply,
+                mImpl.mBroadcastConsumerDeferClear);
+
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason());
+        assertFalse(greenQueue.shouldBeDeferred());
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+        redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+    }
+
+    @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @SuppressWarnings("GuardedBy")
+    @Test
+    public void testDeliveryDeferredForCached_withInfiniteDeferred_flagDisabled() throws Exception {
+        final ProcessRecord greenProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+        final ProcessRecord redProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_RED));
+
+        final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK);
+        final BroadcastOptions optionsTimeTick = BroadcastOptions.makeWithDeferUntilActive(true);
+        final BroadcastRecord timeTickRecord = makeBroadcastRecord(timeTick, optionsTimeTick,
+                List.of(makeRegisteredReceiver(greenProcess, 0)), false /* ordered */);
+
+        final Intent batteryChanged = new Intent(Intent.ACTION_BATTERY_CHANGED);
+        final BroadcastOptions optionsBatteryChanged =
+                BroadcastOptions.makeWithDeferUntilActive(true);
+        final BroadcastRecord batteryChangedRecord = makeBroadcastRecord(batteryChanged,
+                optionsBatteryChanged,
+                List.of(makeRegisteredReceiver(greenProcess, 10),
+                        makeRegisteredReceiver(redProcess, 0)),
+                false /* ordered */);
+
+        mImpl.enqueueBroadcastLocked(timeTickRecord);
+        mImpl.enqueueBroadcastLocked(batteryChangedRecord);
+
+        final BroadcastProcessQueue greenQueue = mImpl.getProcessQueue(PACKAGE_GREEN,
+                getUidForPackage(PACKAGE_GREEN));
+        final BroadcastProcessQueue redQueue = mImpl.getProcessQueue(PACKAGE_RED,
+                getUidForPackage(PACKAGE_RED));
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason());
+        assertFalse(greenQueue.shouldBeDeferred());
+        assertEquals(BroadcastProcessQueue.REASON_BLOCKED, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        // Simulate process state change
+        greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */,
+                true /* processFreezable */);
+        greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply,
+                mImpl.mBroadcastConsumerDeferClear);
+
+        assertEquals(BroadcastProcessQueue.REASON_CACHED_INFINITE_DEFER,
+                greenQueue.getRunnableAtReason());
+        assertTrue(greenQueue.shouldBeDeferred());
+        // Once the broadcasts to green process are deferred, broadcasts to red process
+        // shouldn't be blocked anymore.
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        // All broadcasts to green process should be deferred.
+        greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i));
+        }, false /* andRemove */);
+        redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+
+        final Intent packageChanged = new Intent(Intent.ACTION_PACKAGE_CHANGED);
+        final BroadcastOptions optionsPackageChanged =
+                BroadcastOptions.makeWithDeferUntilActive(true);
+        final BroadcastRecord packageChangedRecord = makeBroadcastRecord(packageChanged,
+                optionsPackageChanged,
+                List.of(makeRegisteredReceiver(greenProcess, 0)), false /* ordered */);
+        mImpl.enqueueBroadcastLocked(packageChangedRecord);
+
+        assertEquals(BroadcastProcessQueue.REASON_CACHED_INFINITE_DEFER,
+                greenQueue.getRunnableAtReason());
+        assertTrue(greenQueue.shouldBeDeferred());
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        // All broadcasts to the green process, including the newly enqueued one, should be
+        // deferred.
+        greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i));
+        }, false /* andRemove */);
+        redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+
+        // Simulate process state change
+        greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */,
+                false /* processFreezable */);
+        greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply,
+                mImpl.mBroadcastConsumerDeferClear);
+
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason());
+        assertFalse(greenQueue.shouldBeDeferred());
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+        redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+    }
+
+    @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @Test
+    public void testDeliveryDeferredForCached_withInfiniteDeferred_changeIdDisabled()
+            throws Exception {
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE),
+                eq(getUidForPackage(PACKAGE_GREEN)));
         final ProcessRecord greenProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
         final ProcessRecord redProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_RED));
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index 3aaf2e5..9d92d5f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -21,6 +21,7 @@
 import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_START_RECEIVER;
 import static android.os.UserHandle.USER_SYSTEM;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.server.am.ActivityManagerDebugConfig.LOG_WRITER_INFO;
 import static com.android.server.am.BroadcastProcessQueue.reasonToString;
 import static com.android.server.am.BroadcastRecord.deliveryStateToString;
@@ -45,7 +46,6 @@
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -77,6 +77,8 @@
 import android.os.PowerExemptionManager;
 import android.os.SystemClock;
 import android.os.UserHandle;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.util.ArrayMap;
 import android.util.Log;
@@ -446,7 +448,8 @@
                 callerApp.getPid(), callerApp.info.uid, false, null, null, null, null,
                 AppOpsManager.OP_NONE, options, receivers, callerApp, resultTo,
                 Activity.RESULT_OK, null, resultExtras, ordered, false, false, userId,
-                BackgroundStartPrivileges.NONE, false, null, PROCESS_STATE_UNKNOWN);
+                BackgroundStartPrivileges.NONE, false, null, PROCESS_STATE_UNKNOWN,
+                mPlatformCompat);
     }
 
     private void assertHealth() {
@@ -1495,7 +1498,7 @@
                 null, null, null, null, AppOpsManager.OP_NONE, BroadcastOptions.makeBasic(),
                 List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN)), null, null,
                 Activity.RESULT_OK, null, null, false, false, false, UserHandle.USER_SYSTEM,
-                backgroundStartPrivileges, false, null, PROCESS_STATE_UNKNOWN);
+                backgroundStartPrivileges, false, null, PROCESS_STATE_UNKNOWN, mPlatformCompat);
         enqueueBroadcast(r);
 
         waitForIdle();
@@ -1550,8 +1553,10 @@
     /**
      * Verify that when dispatching we respect tranches of priority.
      */
+    @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @SuppressWarnings("DistinctVarargsChecker")
     @Test
-    public void testPriority() throws Exception {
+    public void testPriority_flagDisabled() throws Exception {
         final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
         final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
         final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN);
@@ -1594,6 +1599,106 @@
     }
 
     /**
+     * Verify that when dispatching we respect tranches of priority.
+     */
+    @SuppressWarnings("DistinctVarargsChecker")
+    @Test
+    public void testOrdered_withPriorities() throws Exception {
+        final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+        final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
+        final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN);
+        final ProcessRecord receiverYellowApp = makeActiveProcessRecord(PACKAGE_YELLOW);
+
+        // Enqueue a normal broadcast that will go to several processes, and
+        // then enqueue a foreground broadcast that risks reordering
+        final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED);
+        final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        airplane.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+        final IIntentReceiver orderedResultTo = mock(IIntentReceiver.class);
+        enqueueBroadcast(makeOrderedBroadcastRecord(timezone, callerApp,
+                List.of(makeRegisteredReceiver(receiverBlueApp, 10),
+                        makeRegisteredReceiver(receiverGreenApp, 10),
+                        makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE),
+                        makeManifestReceiver(PACKAGE_YELLOW, CLASS_YELLOW),
+                        makeRegisteredReceiver(receiverYellowApp, -10)),
+                orderedResultTo, null));
+        enqueueBroadcast(makeBroadcastRecord(airplane, callerApp,
+                List.of(makeRegisteredReceiver(receiverBlueApp))));
+        waitForIdle();
+
+        // Ignore the final foreground broadcast
+        mScheduledBroadcasts.remove(makeScheduledBroadcast(receiverBlueApp, airplane));
+        assertEquals(6, mScheduledBroadcasts.size());
+
+        // We're only concerned about enforcing ordering between tranches;
+        // within a tranche we're okay with reordering
+        assertEquals(
+                Set.of(makeScheduledBroadcast(receiverBlueApp, timezone),
+                        makeScheduledBroadcast(receiverGreenApp, timezone)),
+                Set.of(mScheduledBroadcasts.remove(0),
+                        mScheduledBroadcasts.remove(0)));
+        assertEquals(
+                Set.of(makeScheduledBroadcast(receiverBlueApp, timezone),
+                        makeScheduledBroadcast(receiverYellowApp, timezone)),
+                Set.of(mScheduledBroadcasts.remove(0),
+                        mScheduledBroadcasts.remove(0)));
+        assertEquals(
+                Set.of(makeScheduledBroadcast(receiverYellowApp, timezone)),
+                Set.of(mScheduledBroadcasts.remove(0)));
+    }
+
+    /**
+     * Verify that when dispatching we respect tranches of priority.
+     */
+    @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @SuppressWarnings("DistinctVarargsChecker")
+    @Test
+    public void testPriority_changeIdDisabled() throws Exception {
+        final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+        final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
+        final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN);
+        final ProcessRecord receiverYellowApp = makeActiveProcessRecord(PACKAGE_YELLOW);
+
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(receiverBlueApp.uid));
+
+        // Enqueue a normal broadcast that will go to several processes, and
+        // then enqueue a foreground broadcast that risks reordering
+        final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED);
+        final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        airplane.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+        enqueueBroadcast(makeBroadcastRecord(timezone, callerApp,
+                List.of(makeRegisteredReceiver(receiverBlueApp, 10),
+                        makeRegisteredReceiver(receiverGreenApp, 10),
+                        makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE),
+                        makeManifestReceiver(PACKAGE_YELLOW, CLASS_YELLOW),
+                        makeRegisteredReceiver(receiverYellowApp, -10))));
+        enqueueBroadcast(makeBroadcastRecord(airplane, callerApp,
+                List.of(makeRegisteredReceiver(receiverBlueApp))));
+        waitForIdle();
+
+        // Ignore the final foreground broadcast
+        mScheduledBroadcasts.remove(makeScheduledBroadcast(receiverBlueApp, airplane));
+        assertEquals(5, mScheduledBroadcasts.size());
+
+        // We're only concerned about enforcing ordering between tranches;
+        // within a tranche we're okay with reordering
+        assertEquals(
+                Set.of(makeScheduledBroadcast(receiverBlueApp, timezone),
+                        makeScheduledBroadcast(receiverGreenApp, timezone)),
+                Set.of(mScheduledBroadcasts.remove(0),
+                        mScheduledBroadcasts.remove(0)));
+        assertEquals(
+                Set.of(makeScheduledBroadcast(receiverBlueApp, timezone),
+                        makeScheduledBroadcast(receiverYellowApp, timezone)),
+                Set.of(mScheduledBroadcasts.remove(0),
+                        mScheduledBroadcasts.remove(0)));
+        assertEquals(
+                Set.of(makeScheduledBroadcast(receiverYellowApp, timezone)),
+                Set.of(mScheduledBroadcasts.remove(0)));
+    }
+
+    /**
      * Verify prioritized receivers work as expected with deferrable broadcast - broadcast to
      * app in cached state should be deferred and the rest should be delivered as per the priority
      * order.
@@ -2305,8 +2410,9 @@
                 .isLessThan(getReceiverScheduledTime(timeTickRecord, receiverBlue));
     }
 
+    @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
     @Test
-    public void testPrioritizedBroadcastDelivery_uidForeground() throws Exception {
+    public void testPrioritizedBroadcastDelivery_uidForeground_flagDisabled() throws Exception {
         final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
         final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
         final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN);
@@ -2332,6 +2438,63 @@
     }
 
     @Test
+    public void testOrderedBroadcastDelivery_uidForeground() throws Exception {
+        final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+        final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
+        final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN);
+
+        mUidObserver.onUidStateChanged(receiverGreenApp.info.uid,
+                ActivityManager.PROCESS_STATE_TOP, 0, ActivityManager.PROCESS_CAPABILITY_NONE);
+        waitForIdle();
+
+        final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK);
+
+        final BroadcastFilter receiverBlue = makeRegisteredReceiver(receiverBlueApp, 10);
+        final BroadcastFilter receiverGreen = makeRegisteredReceiver(receiverGreenApp, 5);
+        final IIntentReceiver resultTo = mock(IIntentReceiver.class);
+        final BroadcastRecord prioritizedRecord = makeOrderedBroadcastRecord(timeTick, callerApp,
+                List.of(receiverBlue, receiverGreen), resultTo, null);
+
+        enqueueBroadcast(prioritizedRecord);
+
+        waitForIdle();
+        // Verify that uid foreground-ness does not impact that delivery of prioritized broadcast.
+        // That is, broadcast to receiverBlueApp gets scheduled before the one to receiverGreenApp.
+        assertThat(getReceiverScheduledTime(prioritizedRecord, receiverGreen))
+                .isGreaterThan(getReceiverScheduledTime(prioritizedRecord, receiverBlue));
+    }
+
+    @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @Test
+    public void testPrioritizedBroadcastDelivery_uidForeground_changeIdDisabled() throws Exception {
+        final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+        final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
+        final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN);
+
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(receiverBlueApp.uid));
+
+        mUidObserver.onUidStateChanged(receiverGreenApp.info.uid,
+                ActivityManager.PROCESS_STATE_TOP, 0, ActivityManager.PROCESS_CAPABILITY_NONE);
+        waitForIdle();
+
+        final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK);
+
+        final BroadcastFilter receiverBlue = makeRegisteredReceiver(receiverBlueApp, 10);
+        final BroadcastFilter receiverGreen = makeRegisteredReceiver(receiverGreenApp, 5);
+        final BroadcastRecord prioritizedRecord = makeBroadcastRecord(timeTick, callerApp,
+                List.of(receiverBlue, receiverGreen));
+
+        enqueueBroadcast(prioritizedRecord);
+
+        waitForIdle();
+        // Verify that uid foreground-ness does not impact that delivery of prioritized broadcast.
+        // That is, broadcast to receiverBlueApp gets scheduled before the one to receiverGreenApp.
+        assertThat(getReceiverScheduledTime(prioritizedRecord, receiverGreen))
+                .isGreaterThan(getReceiverScheduledTime(prioritizedRecord, receiverBlue));
+    }
+
+    @Test
     @RequiresFlagsEnabled(Flags.FLAG_DEFER_OUTGOING_BROADCASTS)
     public void testDeferOutgoingBroadcasts() throws Exception {
         final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java
index 8cd0da7..4a370a3 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java
@@ -18,6 +18,8 @@
 
 import static android.app.ActivityManager.PROCESS_STATE_UNKNOWN;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.server.am.BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE;
 import static com.android.server.am.BroadcastRecord.DELIVERY_DEFERRED;
 import static com.android.server.am.BroadcastRecord.DELIVERY_DELIVERED;
 import static com.android.server.am.BroadcastRecord.DELIVERY_PENDING;
@@ -33,6 +35,8 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
 
 import android.app.BackgroundStartPrivileges;
 import android.app.BroadcastOptions;
@@ -46,11 +50,17 @@
 import android.os.PersistableBundle;
 import android.os.Process;
 import android.os.UserHandle;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.telephony.SubscriptionManager;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.server.compat.PlatformCompat;
+
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -73,6 +83,9 @@
 public class BroadcastRecordTest {
     private static final String TAG = "BroadcastRecordTest";
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     private static final int USER0 = UserHandle.USER_SYSTEM;
     private static final String PACKAGE1 = "pkg1";
     private static final String PACKAGE2 = "pkg2";
@@ -89,10 +102,14 @@
 
     @Mock BroadcastQueue mQueue;
     @Mock ProcessRecord mProcess;
+    @Mock PlatformCompat mPlatformCompat;
 
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
+
+        doReturn(true).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), anyInt());
     }
 
     @Test
@@ -108,13 +125,13 @@
 
         assertArrayEquals(new int[] {-1},
                 calculateBlockedUntilBeyondCount(List.of(
-                        createResolveInfo(PACKAGE1, getAppId(1), 0)), false));
+                        createResolveInfo(PACKAGE1, getAppId(1), 0)), false, mPlatformCompat));
         assertArrayEquals(new int[] {-1},
                 calculateBlockedUntilBeyondCount(List.of(
-                        createResolveInfo(PACKAGE1, getAppId(1), -10)), false));
+                        createResolveInfo(PACKAGE1, getAppId(1), -10)), false, mPlatformCompat));
         assertArrayEquals(new int[] {-1},
                 calculateBlockedUntilBeyondCount(List.of(
-                        createResolveInfo(PACKAGE1, getAppId(1), 10)), false));
+                        createResolveInfo(PACKAGE1, getAppId(1), 10)), false, mPlatformCompat));
     }
 
     @Test
@@ -128,18 +145,19 @@
                 createResolveInfo(PACKAGE2, getAppId(2), 10),
                 createResolveInfo(PACKAGE3, getAppId(3), 10))));
 
-        assertArrayEquals(new int[] {-1,-1,-1},
+        assertArrayEquals(new int[] {-1, -1, -1},
                 calculateBlockedUntilBeyondCount(List.of(
                         createResolveInfo(PACKAGE1, getAppId(1), 0),
                         createResolveInfo(PACKAGE2, getAppId(2), 0),
-                        createResolveInfo(PACKAGE3, getAppId(3), 0)), false));
-        assertArrayEquals(new int[] {-1,-1,-1},
+                        createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {-1, -1, -1},
                 calculateBlockedUntilBeyondCount(List.of(
                         createResolveInfo(PACKAGE1, getAppId(1), 10),
                         createResolveInfo(PACKAGE2, getAppId(2), 10),
-                        createResolveInfo(PACKAGE3, getAppId(3), 10)), false));
+                        createResolveInfo(PACKAGE3, getAppId(3), 10)), false, mPlatformCompat));
     }
 
+    @DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
     @Test
     public void testIsPrioritized_Yes() {
         assertTrue(isPrioritized(List.of(
@@ -151,18 +169,203 @@
                 createResolveInfo(PACKAGE2, getAppId(2), 0),
                 createResolveInfo(PACKAGE3, getAppId(3), 0))));
 
-        assertArrayEquals(new int[] {0,1,2},
+        assertArrayEquals(new int[] {0, 1, 2},
                 calculateBlockedUntilBeyondCount(List.of(
                         createResolveInfo(PACKAGE1, getAppId(1), 10),
                         createResolveInfo(PACKAGE2, getAppId(2), 0),
-                        createResolveInfo(PACKAGE3, getAppId(3), -10)), false));
-        assertArrayEquals(new int[] {0,0,2,3,3},
+                        createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {0, 0, 2, 3, 3},
                 calculateBlockedUntilBeyondCount(List.of(
                         createResolveInfo(PACKAGE1, getAppId(1), 20),
                         createResolveInfo(PACKAGE2, getAppId(2), 20),
                         createResolveInfo(PACKAGE3, getAppId(3), 10),
                         createResolveInfo(PACKAGE3, getAppId(3), 0),
-                        createResolveInfo(PACKAGE3, getAppId(3), 0)), false));
+                        createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat));
+    }
+
+    @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @Test
+    public void testIsPrioritized_withDifferentPriorities() {
+        assertFalse(isPrioritized(List.of(
+                createResolveInfo(PACKAGE1, getAppId(1), 10),
+                createResolveInfo(PACKAGE2, getAppId(2), 0),
+                createResolveInfo(PACKAGE3, getAppId(3), -10))));
+        assertFalse(isPrioritized(List.of(
+                createResolveInfo(PACKAGE1, getAppId(1), 10),
+                createResolveInfo(PACKAGE2, getAppId(2), 0),
+                createResolveInfo(PACKAGE3, getAppId(3), 0))));
+
+        assertArrayEquals(new int[] {-1, -1, -1},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 10),
+                        createResolveInfo(PACKAGE2, getAppId(2), 0),
+                        createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {-1, -1, -1},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 10),
+                        createResolveInfo(PACKAGE2, getAppId(2), 10),
+                        createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {-1, -1, -1},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 10),
+                        createResolveInfo(PACKAGE2, getAppId(2), 0),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {-1, -1, -1, -1, -1},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 20),
+                        createResolveInfo(PACKAGE2, getAppId(2), 20),
+                        createResolveInfo(PACKAGE3, getAppId(3), 10),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat));
+    }
+
+    @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @Test
+    public void testIsPrioritized_withDifferentPriorities_withFirstUidChangeIdDisabled() {
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(1)));
+
+        assertTrue(isPrioritized(List.of(
+                createResolveInfo(PACKAGE1, getAppId(1), 10),
+                createResolveInfo(PACKAGE2, getAppId(2), 0),
+                createResolveInfo(PACKAGE3, getAppId(3), -10))));
+        assertTrue(isPrioritized(List.of(
+                createResolveInfo(PACKAGE1, getAppId(1), 10),
+                createResolveInfo(PACKAGE2, getAppId(2), 0),
+                createResolveInfo(PACKAGE3, getAppId(3), 0))));
+
+        assertArrayEquals(new int[] {0, 1, 1},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 10),
+                        createResolveInfo(PACKAGE2, getAppId(2), 0),
+                        createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {0, 0, 1},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 10),
+                        createResolveInfo(PACKAGE2, getAppId(2), 10),
+                        createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {0, 0, 1, 1, 1},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 20),
+                        createResolveInfo(PACKAGE2, getAppId(2), 20),
+                        createResolveInfo(PACKAGE3, getAppId(3), 10),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat));
+    }
+
+    @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @Test
+    public void testIsPrioritized_withDifferentPriorities_withLastUidChangeIdDisabled() {
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(3)));
+
+        assertTrue(isPrioritized(List.of(
+                createResolveInfo(PACKAGE1, getAppId(1), 10),
+                createResolveInfo(PACKAGE2, getAppId(2), 0),
+                createResolveInfo(PACKAGE3, getAppId(3), -10))));
+        assertTrue(isPrioritized(List.of(
+                createResolveInfo(PACKAGE1, getAppId(1), 10),
+                createResolveInfo(PACKAGE2, getAppId(2), 0),
+                createResolveInfo(PACKAGE3, getAppId(3), 0))));
+
+        assertArrayEquals(new int[] {0, 0, 2},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 10),
+                        createResolveInfo(PACKAGE2, getAppId(2), 0),
+                        createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {0, 1},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 10),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {0, 0, 1},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 10),
+                        createResolveInfo(PACKAGE2, getAppId(2), 0),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {0, 0, 2, 3, 3},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 20),
+                        createResolveInfo(PACKAGE2, getAppId(2), 20),
+                        createResolveInfo(PACKAGE3, getAppId(3), 10),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat));
+    }
+
+    @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @Test
+    public void testIsPrioritized_withDifferentPriorities_withUidChangeIdDisabled() {
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(2)));
+
+        assertTrue(isPrioritized(List.of(
+                createResolveInfo(PACKAGE1, getAppId(1), 10),
+                createResolveInfo(PACKAGE2, getAppId(2), 0),
+                createResolveInfo(PACKAGE3, getAppId(3), -10))));
+        assertTrue(isPrioritized(List.of(
+                createResolveInfo(PACKAGE1, getAppId(1), 10),
+                createResolveInfo(PACKAGE2, getAppId(2), 0),
+                createResolveInfo(PACKAGE3, getAppId(3), 0))));
+
+        assertArrayEquals(new int[] {0, 1, 2},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 10),
+                        createResolveInfo(PACKAGE2, getAppId(2), 0),
+                        createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {0, 1, 0},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 10),
+                        createResolveInfo(PACKAGE2, getAppId(2), 0),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {0, 0, 2, 2, 2},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 20),
+                        createResolveInfo(PACKAGE2, getAppId(2), 20),
+                        createResolveInfo(PACKAGE3, getAppId(3), 10),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0),
+                        createResolveInfo(PACKAGE3, getAppId(4), 0)), false, mPlatformCompat));
+    }
+
+    @EnableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
+    @Test
+    public void testIsPrioritized_withDifferentPriorities_withMultipleUidChangeIdDisabled() {
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(1)));
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(2)));
+
+        assertTrue(isPrioritized(List.of(
+                createResolveInfo(PACKAGE1, getAppId(1), 10),
+                createResolveInfo(PACKAGE2, getAppId(2), 0),
+                createResolveInfo(PACKAGE3, getAppId(3), -10))));
+        assertTrue(isPrioritized(List.of(
+                createResolveInfo(PACKAGE1, getAppId(1), 10),
+                createResolveInfo(PACKAGE2, getAppId(2), 0),
+                createResolveInfo(PACKAGE3, getAppId(3), 0))));
+
+        assertArrayEquals(new int[] {0, 1, 2},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 10),
+                        createResolveInfo(PACKAGE2, getAppId(2), 0),
+                        createResolveInfo(PACKAGE3, getAppId(3), -10)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {0, 1, 1},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 10),
+                        createResolveInfo(PACKAGE2, getAppId(2), 0),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {0, 0, 2, 2, 2},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 20),
+                        createResolveInfo(PACKAGE2, getAppId(2), 20),
+                        createResolveInfo(PACKAGE3, getAppId(3), 10),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0),
+                        createResolveInfo(PACKAGE3, getAppId(4), 0)), false, mPlatformCompat));
+        assertArrayEquals(new int[] {0, 0, 1, 1, 3},
+                calculateBlockedUntilBeyondCount(List.of(
+                        createResolveInfo(PACKAGE1, getAppId(1), 20),
+                        createResolveInfo(PACKAGE2, getAppId(3), 20),
+                        createResolveInfo(PACKAGE3, getAppId(3), 10),
+                        createResolveInfo(PACKAGE3, getAppId(3), 0),
+                        createResolveInfo(PACKAGE3, getAppId(2), 0)), false, mPlatformCompat));
     }
 
     @Test
@@ -602,6 +805,66 @@
         assertTrue(record3.matchesDeliveryGroup(record1));
     }
 
+    @Test
+    public void testCalculateChangeStateForReceivers() {
+        assertArrayEquals(new boolean[] {true, true, true}, calculateChangeState(
+                List.of(createResolveInfo(PACKAGE1, getAppId(1)),
+                        createResolveInfo(PACKAGE2, getAppId(2)),
+                        createResolveInfo(PACKAGE3, getAppId(3)))));
+        assertArrayEquals(new boolean[] {true, true, true, true}, calculateChangeState(
+                List.of(createResolveInfo(PACKAGE1, getAppId(1)),
+                        createResolveInfo(PACKAGE2, getAppId(2)),
+                        createResolveInfo(PACKAGE2, getAppId(2)),
+                        createResolveInfo(PACKAGE3, getAppId(3)))));
+
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(1)));
+        assertArrayEquals(new boolean[] {false, true, true}, calculateChangeState(
+                List.of(createResolveInfo(PACKAGE1, getAppId(1)),
+                        createResolveInfo(PACKAGE2, getAppId(2)),
+                        createResolveInfo(PACKAGE3, getAppId(3)))));
+        assertArrayEquals(new boolean[] {false, true, false, true}, calculateChangeState(
+                List.of(createResolveInfo(PACKAGE1, getAppId(1)),
+                        createResolveInfo(PACKAGE2, getAppId(2)),
+                        createResolveInfo(PACKAGE2, getAppId(1)),
+                        createResolveInfo(PACKAGE3, getAppId(3)))));
+
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(2)));
+        assertArrayEquals(new boolean[] {false, false, true}, calculateChangeState(
+                List.of(createResolveInfo(PACKAGE1, getAppId(1)),
+                        createResolveInfo(PACKAGE2, getAppId(2)),
+                        createResolveInfo(PACKAGE3, getAppId(3)))));
+        assertArrayEquals(new boolean[] {false, true, false, false, false, true},
+                calculateChangeState(
+                        List.of(createResolveInfo(PACKAGE1, getAppId(1)),
+                                createResolveInfo(PACKAGE3, getAppId(3)),
+                                createResolveInfo(PACKAGE2, getAppId(2)),
+                                createResolveInfo(PACKAGE2, getAppId(1)),
+                                createResolveInfo(PACKAGE2, getAppId(2)),
+                                createResolveInfo(PACKAGE3, getAppId(3)))));
+
+        doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging(
+                eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(3)));
+        assertArrayEquals(new boolean[] {false, false, false}, calculateChangeState(
+                List.of(createResolveInfo(PACKAGE1, getAppId(1)),
+                        createResolveInfo(PACKAGE2, getAppId(2)),
+                        createResolveInfo(PACKAGE3, getAppId(3)))));
+        assertArrayEquals(new boolean[] {false, false, false, false, false, false},
+                calculateChangeState(
+                        List.of(createResolveInfo(PACKAGE1, getAppId(1)),
+                                createResolveInfo(PACKAGE3, getAppId(3)),
+                                createResolveInfo(PACKAGE2, getAppId(2)),
+                                createResolveInfo(PACKAGE2, getAppId(1)),
+                                createResolveInfo(PACKAGE2, getAppId(2)),
+                                createResolveInfo(PACKAGE3, getAppId(3)))));
+    }
+
+    private boolean[] calculateChangeState(List<Object> receivers) {
+        return BroadcastRecord.calculateChangeStateForReceivers(receivers,
+                CHANGE_LIMIT_PRIORITY_SCOPE, mPlatformCompat);
+    }
+
     private static void cleanupDisabledPackageReceivers(BroadcastRecord record,
             String packageName, int userId) {
         record.cleanupDisabledPackageReceiversLocked(packageName, null /* filterByClasses */,
@@ -753,16 +1016,17 @@
                 BackgroundStartPrivileges.NONE,
                 false /* timeoutExempt */,
                 filterExtrasForReceiver,
-                PROCESS_STATE_UNKNOWN);
+                PROCESS_STATE_UNKNOWN,
+                mPlatformCompat);
     }
 
     private static int getAppId(int i) {
         return Process.FIRST_APPLICATION_UID + i;
     }
 
-    private static boolean isPrioritized(List<Object> receivers) {
+    private boolean isPrioritized(List<Object> receivers) {
         return BroadcastRecord.isPrioritized(
-                calculateBlockedUntilBeyondCount(receivers, false), false);
+                calculateBlockedUntilBeyondCount(receivers, false, mPlatformCompat), false);
     }
 
     private static void assertBlocked(BroadcastRecord r, boolean... blocked) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/InstallDependencyHelperTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/InstallDependencyHelperTest.java
new file mode 100644
index 0000000..f6c644e
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/InstallDependencyHelperTest.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pm;
+
+import static android.content.pm.Flags.FLAG_SDK_DEPENDENCY_INSTALLER;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.pm.SharedLibraryInfo;
+import android.content.pm.parsing.ApkLite;
+import android.content.pm.parsing.ApkLiteParseUtils;
+import android.content.pm.parsing.PackageLite;
+import android.content.pm.parsing.result.ParseResult;
+import android.content.pm.parsing.result.ParseTypeImpl;
+import android.os.FileUtils;
+import android.os.OutcomeReceiver;
+import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+
+import androidx.annotation.NonNull;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@Presubmit
+@RunWith(JUnit4.class)
+@RequiresFlagsEnabled(FLAG_SDK_DEPENDENCY_INSTALLER)
+public class InstallDependencyHelperTest {
+
+    @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
+    @Rule public final CheckFlagsRule checkFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    private static final String PUSH_FILE_DIR = "/data/local/tmp/tests/smockingservicestest/pm/";
+    private static final String TEST_APP_USING_SDK1_AND_SDK2 = "HelloWorldUsingSdk1And2.apk";
+
+    @Mock private SharedLibrariesImpl mSharedLibraries;
+    private InstallDependencyHelper mInstallDependencyHelper;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mInstallDependencyHelper = new InstallDependencyHelper(mSharedLibraries);
+    }
+
+    @Test
+    public void testResolveLibraryDependenciesIfNeeded_errorInSharedLibrariesImpl()
+            throws Exception {
+        doThrow(new PackageManagerException(new Exception("xyz")))
+                .when(mSharedLibraries).collectMissingSharedLibraryInfos(any());
+
+        PackageLite pkg = getPackageLite(TEST_APP_USING_SDK1_AND_SDK2);
+        CallbackHelper callback = new CallbackHelper(/*expectSuccess=*/ false);
+        mInstallDependencyHelper.resolveLibraryDependenciesIfNeeded(pkg, callback);
+        callback.assertFailure();
+
+        assertThat(callback.error).hasMessageThat().contains("xyz");
+    }
+
+    @Test
+    public void testResolveLibraryDependenciesIfNeeded_failsToBind() throws Exception {
+        // Return a non-empty list as missing dependency
+        PackageLite pkg = getPackageLite(TEST_APP_USING_SDK1_AND_SDK2);
+        List<SharedLibraryInfo> missingDependency = Collections.singletonList(
+                mock(SharedLibraryInfo.class));
+        when(mSharedLibraries.collectMissingSharedLibraryInfos(eq(pkg)))
+                .thenReturn(missingDependency);
+
+        CallbackHelper callback = new CallbackHelper(/*expectSuccess=*/ false);
+        mInstallDependencyHelper.resolveLibraryDependenciesIfNeeded(pkg, callback);
+        callback.assertFailure();
+
+        assertThat(callback.error).hasMessageThat().contains(
+                "Failed to bind to Dependency Installer");
+    }
+
+
+    @Test
+    public void testResolveLibraryDependenciesIfNeeded_allDependenciesInstalled() throws Exception {
+        // Return an empty list as missing dependency
+        PackageLite pkg = getPackageLite(TEST_APP_USING_SDK1_AND_SDK2);
+        List<SharedLibraryInfo> missingDependency = Collections.emptyList();
+        when(mSharedLibraries.collectMissingSharedLibraryInfos(eq(pkg)))
+                .thenReturn(missingDependency);
+
+        CallbackHelper callback = new CallbackHelper(/*expectSuccess=*/ true);
+        mInstallDependencyHelper.resolveLibraryDependenciesIfNeeded(pkg, callback);
+        callback.assertSuccess();
+    }
+
+    private static class CallbackHelper implements OutcomeReceiver<Void, PackageManagerException> {
+        public PackageManagerException error;
+
+        private final CountDownLatch mWait = new CountDownLatch(1);
+        private final boolean mExpectSuccess;
+
+        CallbackHelper(boolean expectSuccess) {
+            mExpectSuccess = expectSuccess;
+        }
+
+        @Override
+        public void onResult(Void result) {
+            if (!mExpectSuccess) {
+                fail("Expected to fail");
+            }
+            mWait.countDown();
+        }
+
+        @Override
+        public void onError(@NonNull PackageManagerException e) {
+            if (mExpectSuccess) {
+                fail("Expected success but received: " + e);
+            }
+            error = e;
+            mWait.countDown();
+        }
+
+        void assertSuccess() throws Exception {
+            assertThat(mWait.await(1000, TimeUnit.MILLISECONDS)).isTrue();
+            assertThat(error).isNull();
+        }
+
+        void assertFailure() throws Exception {
+            assertThat(mWait.await(1000, TimeUnit.MILLISECONDS)).isTrue();
+            assertThat(error).isNotNull();
+        }
+
+    }
+
+    private PackageLite getPackageLite(String apkFileName) throws Exception {
+        File apkFile = copyApkToTmpDir(TEST_APP_USING_SDK1_AND_SDK2);
+        ParseResult<ApkLite> result = ApkLiteParseUtils.parseApkLite(
+                ParseTypeImpl.forDefaultParsing().reset(), apkFile, 0);
+        assertThat(result.isError()).isFalse();
+        ApkLite baseApk = result.getResult();
+
+        return new PackageLite(/*path=*/ null, baseApk.getPath(), baseApk,
+                /*splitNames=*/ null, /*isFeatureSplits=*/ null, /*usesSplitNames=*/ null,
+                /*configForSplit=*/ null, /*splitApkPaths=*/ null,
+                /*splitRevisionCodes=*/ null, baseApk.getTargetSdkVersion(),
+                /*requiredSplitTypes=*/ null, /*splitTypes=*/ null);
+    }
+
+    private File copyApkToTmpDir(String apkFileName) throws Exception {
+        File outFile = temporaryFolder.newFile(apkFileName);
+        String apkFilePath = PUSH_FILE_DIR + apkFileName;
+        File apkFile = new File(apkFilePath);
+        assertThat(apkFile.exists()).isTrue();
+        try (InputStream is = new FileInputStream(apkFile)) {
+            FileUtils.copyToFileOrThrow(is, outFile);
+        }
+        return outFile;
+    }
+
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java
index 591e8df..71c60ad 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java
@@ -742,10 +742,11 @@
                 /* stagedSessionErrorMessage */ "no error",
                 /* preVerifiedDomains */ null,
                 /* verifierController */ null,
-                /* initialVerificationPolicy */ 
+                /* initialVerificationPolicy */
                 PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED,
                 /* currentVerificationPolicy */
-                PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED);
+                PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED,
+                /* installDependencyHelper */ null);
 
         StagingManager.StagedSession stagedSession = spy(session.mStagedSession);
         doReturn(packageName).when(stagedSession).getPackageName();
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
index 2edde9b..d5b9307 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
@@ -33,6 +33,7 @@
 import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE;
 import static com.android.internal.accessibility.dialog.AccessibilityButtonChooserActivity.EXTRA_TYPE_TO_CHOOSE;
@@ -80,6 +81,7 @@
 import android.content.res.XmlResourceParser;
 import android.graphics.drawable.Icon;
 import android.hardware.display.DisplayManagerGlobal;
+import android.hardware.input.KeyGestureEvent;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
@@ -2183,6 +2185,168 @@
         verify(mockUserContext).getSystemService(EnhancedConfirmationManager.class);
     }
 
+    @Test
+    @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES)
+    public void handleKeyGestureEvent_toggleMagnifier() {
+        mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY);
+        assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE,
+                mA11yms.getCurrentUserIdLocked())).isEmpty();
+
+        mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType(
+                KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION).setAction(
+                        KeyGestureEvent.ACTION_GESTURE_COMPLETE).build());
+
+        assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE,
+                mA11yms.getCurrentUserIdLocked())).containsExactly(MAGNIFICATION_CONTROLLER_NAME);
+
+        // The magnifier will only be toggled on the second event received since the first is
+        // used to toggle the feature on.
+        mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType(
+                KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION).setAction(
+                KeyGestureEvent.ACTION_GESTURE_COMPLETE).build());
+
+        verify(mInputFilter).notifyMagnificationShortcutTriggered(anyInt());
+    }
+
+    @Test
+    @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES)
+    public void handleKeyGestureEvent_activateSelectToSpeak_trustedService() {
+        setupAccessibilityServiceConnection(FLAG_REQUEST_ACCESSIBILITY_BUTTON);
+        mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY);
+
+        final AccessibilityServiceInfo trustedService = mockAccessibilityServiceInfo(
+                new ComponentName("package_a", "class_a"),
+                /* isSystemApp= */ true, /* isAlwaysOnService= */ true);
+        AccessibilityUserState userState = mA11yms.getCurrentUserState();
+        userState.mInstalledServices.add(trustedService);
+        mTestableContext.getOrCreateTestableResources().addOverride(
+                R.string.config_defaultSelectToSpeakService,
+                trustedService.getComponentName().flattenToString());
+        mTestableContext.getOrCreateTestableResources().addOverride(
+                R.array.config_trustedAccessibilityServices,
+                new String[]{trustedService.getComponentName().flattenToString()});
+
+        assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE,
+                mA11yms.getCurrentUserIdLocked())).isEmpty();
+
+        mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType(
+                KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK).setAction(
+                KeyGestureEvent.ACTION_GESTURE_COMPLETE).build());
+
+        assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE,
+                mA11yms.getCurrentUserIdLocked())).containsExactly(
+                trustedService.getComponentName().flattenToString());
+    }
+
+    @Test
+    @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES)
+    public void handleKeyGestureEvent_activateSelectToSpeak_preinstalledService() {
+        setupAccessibilityServiceConnection(FLAG_REQUEST_ACCESSIBILITY_BUTTON);
+        mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY);
+
+        final AccessibilityServiceInfo untrustedService = mockAccessibilityServiceInfo(
+                new ComponentName("package_a", "class_a"),
+                /* isSystemApp= */ true, /* isAlwaysOnService= */ true);
+        AccessibilityUserState userState = mA11yms.getCurrentUserState();
+        userState.mInstalledServices.add(untrustedService);
+        mTestableContext.getOrCreateTestableResources().addOverride(
+                R.string.config_defaultSelectToSpeakService,
+                untrustedService.getComponentName().flattenToString());
+
+        assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE,
+                mA11yms.getCurrentUserIdLocked())).isEmpty();
+
+        mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType(
+                KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK).setAction(
+                KeyGestureEvent.ACTION_GESTURE_COMPLETE).build());
+
+        assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE,
+                mA11yms.getCurrentUserIdLocked())).isEmpty();
+    }
+
+    @Test
+    @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES)
+    public void handleKeyGestureEvent_activateSelectToSpeak_downloadedService() {
+        mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY);
+
+        final AccessibilityServiceInfo downloadedService = mockAccessibilityServiceInfo(
+                new ComponentName("package_a", "class_a"),
+                /* isSystemApp= */ false, /* isAlwaysOnService= */ true);
+        AccessibilityUserState userState = mA11yms.getCurrentUserState();
+        userState.mInstalledServices.add(downloadedService);
+        mTestableContext.getOrCreateTestableResources().addOverride(
+                R.string.config_defaultSelectToSpeakService,
+                downloadedService.getComponentName().flattenToString());
+        mTestableContext.getOrCreateTestableResources().addOverride(
+                R.array.config_trustedAccessibilityServices,
+                new String[]{downloadedService.getComponentName().flattenToString()});
+
+        assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE,
+                mA11yms.getCurrentUserIdLocked())).isEmpty();
+
+        mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType(
+                KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK).setAction(
+                KeyGestureEvent.ACTION_GESTURE_COMPLETE).build());
+
+        assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE,
+                mA11yms.getCurrentUserIdLocked())).isEmpty();
+    }
+
+    @Test
+    @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES)
+    public void handleKeyGestureEvent_activateSelectToSpeak_defaultNotInstalled() {
+        mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY);
+
+        final AccessibilityServiceInfo installedService = mockAccessibilityServiceInfo(
+                new ComponentName("package_a", "class_a"),
+                /* isSystemApp= */ true, /* isAlwaysOnService= */ true);
+        final AccessibilityServiceInfo defaultService = mockAccessibilityServiceInfo(
+                new ComponentName("package_b", "class_b"),
+                /* isSystemApp= */ true, /* isAlwaysOnService= */ true);
+        AccessibilityUserState userState = mA11yms.getCurrentUserState();
+        userState.mInstalledServices.add(installedService);
+        mTestableContext.getOrCreateTestableResources().addOverride(
+                R.string.config_defaultSelectToSpeakService,
+                defaultService.getComponentName().flattenToString());
+        mTestableContext.getOrCreateTestableResources().addOverride(
+                R.array.config_trustedAccessibilityServices,
+                new String[]{defaultService.getComponentName().flattenToString()});
+
+        assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE,
+                mA11yms.getCurrentUserIdLocked())).isEmpty();
+
+        mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType(
+                KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK).setAction(
+                KeyGestureEvent.ACTION_GESTURE_COMPLETE).build());
+
+        assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE,
+                mA11yms.getCurrentUserIdLocked())).isEmpty();
+    }
+
+    @Test
+    @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES)
+    public void handleKeyGestureEvent_activateSelectToSpeak_noDefault() {
+        mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY);
+
+        final AccessibilityServiceInfo installedService = mockAccessibilityServiceInfo(
+                new ComponentName("package_a", "class_a"),
+                /* isSystemApp= */ true, /* isAlwaysOnService= */ true);
+        AccessibilityUserState userState = mA11yms.getCurrentUserState();
+        userState.mInstalledServices.add(installedService);
+        mTestableContext.getOrCreateTestableResources().addOverride(
+                R.array.config_trustedAccessibilityServices,
+                new String[]{installedService.getComponentName().flattenToString()});
+
+        assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE,
+                mA11yms.getCurrentUserIdLocked())).isEmpty();
+
+        mA11yms.handleKeyGestureEvent(new KeyGestureEvent.Builder().setKeyGestureType(
+                KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK).setAction(
+                KeyGestureEvent.ACTION_GESTURE_COMPLETE).build());
+
+        assertThat(ShortcutUtils.getShortcutTargetsFromSettings(mTestableContext, KEY_GESTURE,
+                mA11yms.getCurrentUserIdLocked())).isEmpty();
+    }
 
     private Set<String> readStringsFromSetting(String setting) {
         final Set<String> result = new ArraySet<>();
@@ -2298,6 +2462,10 @@
                 AccessibilityManagerService service) {
             super(context, service);
         }
+
+        @Override
+        void notifyMagnificationShortcutTriggered(int displayId) {
+        }
     }
 
     private static class A11yTestableContext extends TestableContext {
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java
index 8c35925..cb52eef 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java
@@ -32,6 +32,7 @@
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.ALL;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TRIPLETAP;
@@ -174,6 +175,7 @@
         mUserState.updateShortcutTargetsLocked(Set.of(componentNameString), SOFTWARE);
         mUserState.updateShortcutTargetsLocked(Set.of(componentNameString), GESTURE);
         mUserState.updateShortcutTargetsLocked(Set.of(componentNameString), QUICK_SETTINGS);
+        mUserState.updateShortcutTargetsLocked(Set.of(componentNameString), KEY_GESTURE);
         mUserState.updateA11yTilesInQsPanelLocked(
                 Set.of(AccessibilityShortcutController.COLOR_INVERSION_TILE_COMPONENT_NAME));
         mUserState.setTargetAssignedToAccessibilityButton(componentNameString);
@@ -201,6 +203,7 @@
         assertTrue(mUserState.getShortcutTargetsLocked(SOFTWARE).isEmpty());
         assertTrue(mUserState.getShortcutTargetsLocked(GESTURE).isEmpty());
         assertTrue(mUserState.getShortcutTargetsLocked(QUICK_SETTINGS).isEmpty());
+        assertTrue(mUserState.getShortcutTargetsLocked(KEY_GESTURE).isEmpty());
         assertTrue(mUserState.getA11yQsTilesInQsPanel().isEmpty());
         assertNull(mUserState.getTargetAssignedToAccessibilityButton());
         assertFalse(mUserState.isTouchExplorationEnabledLocked());
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
index 411a610..361df94 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
@@ -44,7 +44,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags;
 import com.android.server.UiServiceTestCase;
@@ -89,7 +89,7 @@
 import javax.annotation.Nullable;
 
 @RunWith(AndroidJUnit4.class)
-@EnableFlags({Flags.FLAG_VISIT_PERSON_URI, Flags.FLAG_API_RICH_ONGOING})
+@EnableFlags({Flags.FLAG_API_RICH_ONGOING})
 public class NotificationVisitUrisTest extends UiServiceTestCase {
     @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java
index 9967cce..7dba142 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java
@@ -21,7 +21,6 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyZeroInteractions;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
 
 import static org.junit.Assert.assertEquals;
@@ -165,31 +164,6 @@
     }
 
     @Test
-    public void testDelayingAnimationStart() {
-        mAnimatable.mSurfaceAnimator.startDelayingAnimationStart();
-        mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */,
-                ANIMATION_TYPE_APP_TRANSITION);
-        verifyZeroInteractions(mSpec);
-        assertAnimating(mAnimatable);
-        assertTrue(mAnimatable.mSurfaceAnimator.isAnimationStartDelayed());
-        mAnimatable.mSurfaceAnimator.endDelayingAnimationStart();
-        verify(mSpec).startAnimation(any(), any(), eq(ANIMATION_TYPE_APP_TRANSITION), any());
-    }
-
-    @Test
-    public void testDelayingAnimationStartAndCancelled() {
-        mAnimatable.mSurfaceAnimator.startDelayingAnimationStart();
-        mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */,
-                ANIMATION_TYPE_APP_TRANSITION);
-        mAnimatable.mSurfaceAnimator.cancelAnimation();
-        verifyZeroInteractions(mSpec);
-        assertNotAnimating(mAnimatable);
-        assertTrue(mAnimatable.mFinishedCallbackCalled);
-        assertEquals(ANIMATION_TYPE_APP_TRANSITION, mAnimatable.mFinishedAnimationType);
-        verify(mTransaction).remove(eq(mAnimatable.mLeash));
-    }
-
-    @Test
     public void testTransferAnimation() {
         mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */,
                 ANIMATION_TYPE_APP_TRANSITION);
diff --git a/telephony/java/android/telephony/satellite/EarfcnRange.aidl b/telephony/java/android/telephony/satellite/EarfcnRange.aidl
new file mode 100644
index 0000000..0b224d0
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/EarfcnRange.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telephony.satellite;
+
+parcelable EarfcnRange;
diff --git a/telephony/java/android/telephony/satellite/EarfcnRange.java b/telephony/java/android/telephony/satellite/EarfcnRange.java
new file mode 100644
index 0000000..38043b5
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/EarfcnRange.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telephony.satellite;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.telephony.flags.Flags;
+
+/**
+ * EARFCN (E-UTRA Absolute Radio Frequency Channel Number):  A number that identifies a
+ * specific frequency channel in LTE/5G NR, used to define the carrier frequency.
+ * The range can be [0 ~ 65535] according to the 3GPP TS 36.101
+ *
+ * In satellite communication:
+ * - Efficient frequency allocation across a wide coverage area.
+ * - Handles Doppler shift due to satellite movement.
+ * - Manages interference with terrestrial networks.
+ *
+ * See 3GPP TS 36.101 and 38.101-1 for details.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+public final class EarfcnRange implements Parcelable {
+
+    /**
+     * The start frequency of the earfcn range and is inclusive in the range
+     */
+    private int mStartEarfcn;
+
+    /**
+     * The end frequency of the earfcn range and is inclusive in the range.
+     */
+    private int mEndEarfcn;
+
+    private EarfcnRange(@NonNull Parcel in) {
+        readFromParcel(in);
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mStartEarfcn);
+        dest.writeInt(mEndEarfcn);
+    }
+
+    private void readFromParcel(Parcel in) {
+        mStartEarfcn = in.readInt();
+        mEndEarfcn = in.readInt();
+    }
+
+    /**
+     * Constructor for the EarfcnRange class.
+     * The range can be [0 ~ 65535] according to the 3GPP TS 36.101
+     *
+     * @param startEarfcn The starting earfcn value.
+     * @param endEarfcn   The ending earfcn value.
+     */
+    public EarfcnRange(@IntRange(from = 0, to = 65535) int endEarfcn,
+            @IntRange(from = 0, to = 65535) int startEarfcn) {
+        mEndEarfcn = endEarfcn;
+        mStartEarfcn = startEarfcn;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public String toString() {
+        return "startEarfcn: " + mStartEarfcn + ", " + "endEarfcn: " + mEndEarfcn;
+    }
+
+    @NonNull
+    public static final Creator<EarfcnRange> CREATOR = new Creator<EarfcnRange>() {
+        @Override
+        public EarfcnRange createFromParcel(Parcel in) {
+            return new EarfcnRange(in);
+        }
+
+        @Override
+        public EarfcnRange[] newArray(int size) {
+            return new EarfcnRange[size];
+        }
+    };
+
+    /**
+     * Returns the starting earfcn value for this range.
+     * It can be [0 ~ 65535] according to the 3GPP TS 36.101
+     *
+     * @return The starting earfcn.
+     */
+    public @IntRange(from = 0, to = 65535) int getStartEarfcn() {
+        return mStartEarfcn;
+    }
+
+    /**
+     * Returns the ending earfcn value for this range.
+     * It can be [0 ~ 65535] according to the 3GPP TS 36.101
+     *
+     * @return The ending earfcn.
+     */
+    public @IntRange(from = 0, to = 65535) int getEndEarfcn() {
+        return mEndEarfcn;
+    }
+}
diff --git a/telephony/java/android/telephony/satellite/ISatelliteCommunicationAllowedStateCallback.aidl b/telephony/java/android/telephony/satellite/ISatelliteCommunicationAllowedStateCallback.aidl
index a7eda48..2730f90 100644
--- a/telephony/java/android/telephony/satellite/ISatelliteCommunicationAllowedStateCallback.aidl
+++ b/telephony/java/android/telephony/satellite/ISatelliteCommunicationAllowedStateCallback.aidl
@@ -16,6 +16,8 @@
 
 package android.telephony.satellite;
 
+import android.telephony.satellite.SatelliteAccessConfiguration;
+
 /**
  * Interface for satellite communication allowed state callback.
  * @hide
@@ -29,4 +31,14 @@
      * @param allowed whether satellite communication state or not
      */
     void onSatelliteCommunicationAllowedStateChanged(in boolean isAllowed);
+
+    /**
+     * Callback method invoked when the satellite access configuration changes
+     *
+     * @param The satellite access configuration associated with the current location.
+     * When satellite is not allowed at the current location,
+     * {@code satelliteRegionalConfiguration} will be null.
+     */
+    void onSatelliteAccessConfigurationChanged(in SatelliteAccessConfiguration
+        satelliteAccessConfiguration);
 }
diff --git a/telephony/java/android/telephony/satellite/SatelliteAccessConfiguration.aidl b/telephony/java/android/telephony/satellite/SatelliteAccessConfiguration.aidl
new file mode 100644
index 0000000..0214193
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/SatelliteAccessConfiguration.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+ package android.telephony.satellite;
+
+ parcelable SatelliteAccessConfiguration;
\ No newline at end of file
diff --git a/telephony/java/android/telephony/satellite/SatelliteAccessConfiguration.java b/telephony/java/android/telephony/satellite/SatelliteAccessConfiguration.java
new file mode 100644
index 0000000..c3ae70b
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/SatelliteAccessConfiguration.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telephony.satellite;
+
+import android.annotation.FlaggedApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.telephony.flags.Flags;
+
+import java.util.List;
+
+/**
+ * SatelliteAccessConfiguration is used to store satellite access configuration
+ * that will be applied to the satellite communication at the corresponding region.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+public final class SatelliteAccessConfiguration implements Parcelable {
+    /**
+     * The list of satellites available at the current location.
+     */
+    @NonNull
+    private List<SatelliteInfo> mSatelliteInfoList;
+
+    /**
+     * The list of tag IDs associated with the current location
+     */
+    @NonNull
+    private int[] mTagIds;
+
+    /**
+     * Constructor for {@link SatelliteAccessConfiguration}.
+     *
+     * @param satelliteInfos The list of {@link SatelliteInfo} objects representing the satellites
+     *                       accessible with this configuration.
+     * @param tagIds         The list of tag IDs associated with this configuration.
+     */
+    public SatelliteAccessConfiguration(@NonNull List<SatelliteInfo> satelliteInfos,
+            @NonNull int[] tagIds) {
+        mSatelliteInfoList = satelliteInfos;
+        mTagIds = tagIds;
+    }
+
+    public SatelliteAccessConfiguration(Parcel in) {
+        mSatelliteInfoList = in.createTypedArrayList(SatelliteInfo.CREATOR);
+        mTagIds = new int[in.readInt()];
+        in.readIntArray(mTagIds);
+    }
+
+    public static final Creator<SatelliteAccessConfiguration> CREATOR =
+            new Creator<SatelliteAccessConfiguration>() {
+                @Override
+                public SatelliteAccessConfiguration createFromParcel(Parcel in) {
+                    return new SatelliteAccessConfiguration(in);
+                }
+
+                @Override
+                public SatelliteAccessConfiguration[] newArray(int size) {
+                    return new SatelliteAccessConfiguration[size];
+                }
+            };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * @param dest  The Parcel in which the object should be written.
+     * @param flags Additional flags about how the object should be written.
+     *              May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}.
+     */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeTypedList(mSatelliteInfoList);
+        if (mTagIds != null && mTagIds.length > 0) {
+            dest.writeInt(mTagIds.length);
+            dest.writeIntArray(mTagIds);
+        } else {
+            dest.writeInt(0);
+        }
+    }
+
+    /**
+     * Returns a list of {@link SatelliteInfo} objects representing the satellites
+     * associated with this object.
+     *
+     * @return The list of {@link SatelliteInfo} objects.
+     */
+    @NonNull
+    public List<SatelliteInfo> getSatelliteInfos() {
+        return mSatelliteInfoList;
+    }
+
+    /**
+     * Returns a list of tag IDs associated with this object.
+     *
+     * @return The list of tag IDs.
+     */
+    @NonNull
+    public int[] getTagIds() {
+        return mTagIds;
+    }
+}
diff --git a/telephony/java/android/telephony/satellite/SatelliteCommunicationAllowedStateCallback.java b/telephony/java/android/telephony/satellite/SatelliteCommunicationAllowedStateCallback.java
index 1a87020..bffb11f 100644
--- a/telephony/java/android/telephony/satellite/SatelliteCommunicationAllowedStateCallback.java
+++ b/telephony/java/android/telephony/satellite/SatelliteCommunicationAllowedStateCallback.java
@@ -17,6 +17,7 @@
 package android.telephony.satellite;
 
 import android.annotation.FlaggedApi;
+import android.annotation.Nullable;
 
 import com.android.internal.telephony.flags.Flags;
 
@@ -40,4 +41,17 @@
      */
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     void onSatelliteCommunicationAllowedStateChanged(boolean isAllowed);
+
+    /**
+     * Callback method invoked when the satellite access configuration changes
+     *
+     * @param satelliteAccessConfiguration The satellite access configuration associated with
+     *                                       the current location. When satellite is not allowed at
+     *                                       the current location,
+     *                                       {@code satelliteRegionalConfiguration} will be null.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+    default void onSatelliteAccessConfigurationChanged(
+            @Nullable SatelliteAccessConfiguration satelliteAccessConfiguration) {};
 }
diff --git a/telephony/java/android/telephony/satellite/SatelliteInfo.aidl b/telephony/java/android/telephony/satellite/SatelliteInfo.aidl
new file mode 100644
index 0000000..fc2303b
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/SatelliteInfo.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+ package android.telephony.satellite;
+
+ parcelable SatelliteInfo;
\ No newline at end of file
diff --git a/telephony/java/android/telephony/satellite/SatelliteInfo.java b/telephony/java/android/telephony/satellite/SatelliteInfo.java
new file mode 100644
index 0000000..bca907e
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/SatelliteInfo.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telephony.satellite;
+
+import android.annotation.FlaggedApi;
+import android.os.Parcel;
+import android.os.ParcelUuid;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.telephony.flags.Flags;
+
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * SatelliteInfo stores a satellite's identification, position, and frequency information
+ * facilitating efficient satellite communications.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+public class SatelliteInfo implements Parcelable {
+    /**
+     * Unique identification number for the satellite.
+     * This ID is used to distinguish between different satellites in the network.
+     */
+    @NonNull
+    private UUID mId;
+
+    /**
+     * Position information of a satellite.
+     * This includes the longitude and altitude of the satellite.
+     */
+    private SatellitePosition mPosition;
+
+    /**
+     * The frequency bands to scan. Bands and earfcns won't overlap.
+     * Bands will be filled only if the whole band is needed.
+     * Maximum length of the vector is 8.
+     */
+    private int[] mBands;
+
+    /**
+     * EARFCN (E-UTRA Absolute Radio Frequency Channel Number) Ranges
+     * The supported frequency range list.
+     * Maximum length of the vector is 8.
+     */
+    private final List<EarfcnRange> mEarfcnRangeList;
+
+    protected SatelliteInfo(Parcel in) {
+        ParcelUuid parcelUuid = in.readParcelable(
+                ParcelUuid.class.getClassLoader(), ParcelUuid.class);
+        if (parcelUuid != null) {
+            mId = parcelUuid.getUuid();
+        }
+        mPosition = in.readParcelable(SatellitePosition.class.getClassLoader(),
+                SatellitePosition.class);
+        int numBands = in.readInt();
+        mBands = new int[numBands];
+        if (numBands > 0) {
+            for (int i = 0; i < numBands; i++) {
+                mBands[i] = in.readInt();
+            }
+        }
+        mEarfcnRangeList = in.createTypedArrayList(EarfcnRange.CREATOR);
+    }
+
+    /**
+     * Constructor for {@link SatelliteInfo}.
+     *
+     * @param satelliteId       The ID of the satellite.
+     * @param satellitePosition The {@link SatellitePosition} of the satellite.
+     * @param bands             The list of frequency bands supported by the satellite.
+     * @param earfcnRanges      The list of {@link EarfcnRange} objects representing the EARFCN
+     *                          ranges supported by the satellite.
+     */
+    public SatelliteInfo(@NonNull UUID satelliteId, @NonNull SatellitePosition satellitePosition,
+            @NonNull int[] bands, @NonNull List<EarfcnRange> earfcnRanges) {
+        mId = satelliteId;
+        mPosition = satellitePosition;
+        mBands = bands;
+        mEarfcnRangeList = earfcnRanges;
+    }
+
+    public static final Creator<SatelliteInfo> CREATOR = new Creator<SatelliteInfo>() {
+        @Override
+        public SatelliteInfo createFromParcel(Parcel in) {
+            return new SatelliteInfo(in);
+        }
+
+        @Override
+        public SatelliteInfo[] newArray(int size) {
+            return new SatelliteInfo[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeParcelable(new ParcelUuid(mId), flags);
+        dest.writeParcelable(mPosition, flags);
+        if (mBands != null && mBands.length > 0) {
+            dest.writeInt(mBands.length);
+            dest.writeIntArray(mBands);
+        } else {
+            dest.writeInt(0);
+        }
+        dest.writeTypedList(mEarfcnRangeList);
+    }
+
+    /**
+     * Returns the ID of the satellite.
+     *
+     * @return The satellite ID.
+     */
+    @NonNull
+    public UUID getSatelliteId() {
+        return mId;
+    }
+
+    /**
+     * Returns the position of the satellite.
+     *
+     * @return The {@link SatellitePosition} of the satellite.
+     */
+    public SatellitePosition getSatellitePosition() {
+        return mPosition;
+    }
+
+    /**
+     * Returns the list of frequency bands supported by the satellite.
+     *
+     * @return The list of frequency bands.
+     */
+    @NonNull
+    public int[] getBands() {
+        return mBands;
+    }
+
+    /**
+     * Returns the list of EARFCN ranges supported by the satellite.
+     *
+     * @return The list of {@link EarfcnRange} objects.
+     */
+    @NonNull
+    public List<EarfcnRange> getEarfcnRanges() {
+        return mEarfcnRangeList;
+    }
+}
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 88dddcf..7e3d99a 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -36,6 +36,7 @@
 import android.os.OutcomeReceiver;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
+import android.telephony.CarrierConfigManager;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyCallback;
 import android.telephony.TelephonyFrameworkInitializer;
@@ -272,6 +273,14 @@
     public static final String KEY_DEPROVISION_SATELLITE_TOKENS = "deprovision_satellite";
 
     /**
+     * Bundle key to get the response from
+     * {@link #requestSatelliteAccessConfigurationForCurrentLocation(Executor, OutcomeReceiver)}.
+     * @hide
+     */
+    public static final String KEY_SATELLITE_ACCESS_CONFIGURATION =
+            "satellite_access_configuration";
+
+    /**
      * The request was successfully processed.
      * @hide
      */
@@ -2332,6 +2341,68 @@
     }
 
     /**
+     * Request to get satellite access configuration for the current location.
+     *
+     * @param executor The executor on which the callback will be called.
+     * @param callback The callback object to which the result will be delivered.
+     *                 If the request is successful, {@link OutcomeReceiver#onResult(Object)}
+     *                 will return a {@code SatelliteAccessConfiguration} with value the regional
+     *                 satellite access configuration at the current location.
+     *                 If the request is not successful, {@link OutcomeReceiver#onError(Throwable)}
+     *                 will return a {@link SatelliteException} with the {@link SatelliteResult}.
+     *
+     * @throws SecurityException if the caller doesn't have required permission.
+     *
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
+    @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+    public void requestSatelliteAccessConfigurationForCurrentLocation(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OutcomeReceiver<SatelliteAccessConfiguration, SatelliteException> callback) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
+
+        try {
+            ITelephony telephony = getITelephony();
+            if (telephony != null) {
+                ResultReceiver receiver = new ResultReceiver(null) {
+                    @Override
+                    protected void onReceiveResult(int resultCode, Bundle resultData) {
+                        if (resultCode == SATELLITE_RESULT_SUCCESS) {
+                            if (resultData.containsKey(KEY_SATELLITE_ACCESS_CONFIGURATION)) {
+                                SatelliteAccessConfiguration satelliteAccessConfiguration =
+                                        resultData.getParcelable(KEY_SATELLITE_ACCESS_CONFIGURATION,
+                                                SatelliteAccessConfiguration.class);
+                                executor.execute(() -> Binder.withCleanCallingIdentity(() ->
+                                        callback.onResult(satelliteAccessConfiguration)));
+                            } else {
+                                loge("KEY_SATELLITE_ACCESS_CONFIGURATION does not exist.");
+                                executor.execute(() -> Binder.withCleanCallingIdentity(() ->
+                                        callback.onError(new SatelliteException(
+                                                SATELLITE_RESULT_REQUEST_FAILED))));
+                            }
+                        } else {
+                            executor.execute(() -> Binder.withCleanCallingIdentity(() ->
+                                    callback.onError(new SatelliteException(resultCode))));
+                        }
+                    }
+                };
+                telephony.requestSatelliteAccessConfigurationForCurrentLocation(receiver);
+            } else {
+                loge("requestSatelliteAccessConfigurationForCurrentLocation() invalid telephony");
+                executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
+                        new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
+            }
+        } catch (RemoteException ex) {
+            loge("requestSatelliteAccessConfigurationForCurrentLocation() RemoteException: "
+                    + ex);
+            executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
+                    new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
+        }
+    }
+
+    /**
      * Request to get the duration in seconds after which the satellite will be visible.
      * This will be {@link Duration#ZERO} if the satellite is currently visible.
      *
@@ -2436,7 +2507,7 @@
      * <li>There is no satellite communication restriction, which is added by
      * {@link #addAttachRestrictionForCarrier(int, int, Executor, Consumer)}</li>
      * <li>The carrier config {@link
-     * android.telephony.CarrierConfigManager#KEY_SATELLITE_ATTACH_SUPPORTED_BOOL} is set to
+     * CarrierConfigManager#KEY_SATELLITE_ATTACH_SUPPORTED_BOOL} is set to
      * {@code true}.</li>
      * </ul>
      *
@@ -2759,7 +2830,7 @@
      * <p>
      * Note: This API is specifically designed for OEM enabled satellite connectivity only.
      * For satellite connectivity enabled using carrier roaming, please refer to
-     * {@link android.telephony.TelephonyCallback.SignalStrengthsListener}, and
+     * {@link TelephonyCallback.SignalStrengthsListener}, and
      * {@link TelephonyManager#registerTelephonyCallback(Executor, TelephonyCallback)}.
      * </p>
      *
@@ -2830,7 +2901,7 @@
      * <p>
      * Note: This API is specifically designed for OEM enabled satellite connectivity only.
      * For satellite connectivity enabled using carrier roaming, please refer to
-     * {@link android.telephony.TelephonyCallback.SignalStrengthsListener}, and
+     * {@link TelephonyCallback.SignalStrengthsListener}, and
      * {@link TelephonyManager#registerTelephonyCallback(Executor, TelephonyCallback)}.
      * </p>
      *
@@ -3147,6 +3218,15 @@
                                         () -> callback.onSatelliteCommunicationAllowedStateChanged(
                                                 isAllowed)));
                             }
+
+                            @Override
+                            public void onSatelliteAccessConfigurationChanged(
+                                    @Nullable SatelliteAccessConfiguration
+                                            satelliteAccessConfiguration) {
+                                executor.execute(() -> Binder.withCleanCallingIdentity(
+                                        () -> callback.onSatelliteAccessConfigurationChanged(
+                                                satelliteAccessConfiguration)));
+                            }
                         };
                 sSatelliteCommunicationAllowedStateCallbackMap.put(callback, internalCallback);
                 return telephony.registerForCommunicationAllowedStateChanged(
diff --git a/telephony/java/android/telephony/satellite/SatellitePosition.aidl b/telephony/java/android/telephony/satellite/SatellitePosition.aidl
new file mode 100644
index 0000000..a8028eb
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/SatellitePosition.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+ package android.telephony.satellite;
+
+ parcelable SatellitePosition;
\ No newline at end of file
diff --git a/telephony/java/android/telephony/satellite/SatellitePosition.java b/telephony/java/android/telephony/satellite/SatellitePosition.java
new file mode 100644
index 0000000..1e8c018
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/SatellitePosition.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.telephony.satellite;
+
+import android.annotation.FlaggedApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.telephony.flags.Flags;
+
+/**
+ * The position of a satellite in Earth orbit.
+ *
+ * Longitude is the angular distance, measured in degrees, east or west of the prime longitude line
+ * ranging from -180 to 180 degrees
+ * Altitude is the distance from the center of the Earth to the satellite, measured in kilometers
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+public class SatellitePosition implements Parcelable {
+
+    /**
+     * The longitude of the satellite in degrees, ranging from -180 to 180 degrees
+     */
+    private double mLongitudeDegree;
+
+    /**
+     * The distance from the center of the earth to the satellite, measured in kilometers
+     */
+    private double mAltitudeKm;
+
+    /**
+     * Constructor for {@link SatellitePosition} used to create an instance from a {@link Parcel}.
+     *
+     * @param in The {@link Parcel} to read the satellite position data from.
+     */
+    public SatellitePosition(Parcel in) {
+        mLongitudeDegree = in.readDouble();
+        mAltitudeKm = in.readDouble();
+    }
+
+    /**
+     * Constructor for {@link SatellitePosition}.
+     *
+     * @param longitudeDegree The longitude of the satellite in degrees.
+     * @param altitudeKm  The altitude of the satellite in kilometers.
+     */
+    public SatellitePosition(double longitudeDegree, double altitudeKm) {
+        mLongitudeDegree = longitudeDegree;
+        mAltitudeKm = altitudeKm;
+    }
+
+    public static final Creator<SatellitePosition> CREATOR = new Creator<SatellitePosition>() {
+        @Override
+        public SatellitePosition createFromParcel(Parcel in) {
+            return new SatellitePosition(in);
+        }
+
+        @Override
+        public SatellitePosition[] newArray(int size) {
+            return new SatellitePosition[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * @param dest  The Parcel in which the object should be written.
+     * @param flags Additional flags about how the object should be written.
+     *              May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}.
+     */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeDouble(mLongitudeDegree);
+        dest.writeDouble(mAltitudeKm);
+    }
+
+    /**
+     * Returns the longitude of the satellite in degrees, ranging from -180 to 180 degrees.
+     *
+     * @return The longitude of the satellite.
+     */
+    public double getLongitudeDegrees() {
+        return mLongitudeDegree;
+    }
+
+    /**
+     * Returns the altitude of the satellite in kilometers
+     *
+     * @return The altitude of the satellite.
+     */
+    public double getAltitudeKm() {
+        return mAltitudeKm;
+    }
+}
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 544bfab..210200b 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -2999,6 +2999,16 @@
     void requestIsCommunicationAllowedForCurrentLocation(int subId, in ResultReceiver receiver);
 
     /**
+     * Request to get satellite access configuration for the current location.
+     *
+     * @param receiver Result receiver to get the error code of the request
+     *                 and satellite access configuration for the current location.
+     */
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+            + "android.Manifest.permission.SATELLITE_COMMUNICATION)")
+    void requestSatelliteAccessConfigurationForCurrentLocation(in ResultReceiver receiver);
+
+    /**
      * Request to get the time after which the satellite will be visible.
      *
      * @param receiver Result receiver to get the error code of the request and the requested
diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
index 9a9a331..ea61ad9 100644
--- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
@@ -28,6 +28,7 @@
 import android.tools.traces.wm.WindowingMode
 import android.view.WindowInsets
 import android.view.WindowManager
+import android.window.DesktopModeFlags
 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.BySelector
@@ -35,7 +36,6 @@
 import androidx.test.uiautomator.UiObject2
 import androidx.test.uiautomator.Until
 import com.android.server.wm.flicker.helpers.MotionEventHelper.InputMethod.TOUCH
-import com.android.window.flags.Flags
 import java.time.Duration
 
 /**
@@ -107,7 +107,7 @@
 
         // drag the window to move to desktop
         if (motionEventHelper.inputMethod == TOUCH
-            && Flags.enableHoldToDragAppHandle()) {
+            && DesktopModeFlags.ENABLE_HOLD_TO_DRAG_APP_HANDLE.isTrue) {
             // Touch requires hold-to-drag.
             motionEventHelper.holdToDrag(startX, startY, startX, endY, steps = 100)
         } else {
diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
index 61400ed..09a686c 100644
--- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
@@ -782,6 +782,30 @@
                 KeyEvent.META_META_ON or KeyEvent.META_ALT_ON,
                 intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
             ),
+            TestData(
+                "META + ALT + M -> Toggle Magnification",
+                intArrayOf(
+                    KeyEvent.KEYCODE_META_LEFT,
+                    KeyEvent.KEYCODE_ALT_LEFT,
+                    KeyEvent.KEYCODE_M
+                ),
+                KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION,
+                intArrayOf(KeyEvent.KEYCODE_M),
+                KeyEvent.META_META_ON or KeyEvent.META_ALT_ON,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            ),
+            TestData(
+                "META + ALT + S -> Activate Select to Speak",
+                intArrayOf(
+                    KeyEvent.KEYCODE_META_LEFT,
+                    KeyEvent.KEYCODE_ALT_LEFT,
+                    KeyEvent.KEYCODE_S
+                ),
+                KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK,
+                intArrayOf(KeyEvent.KEYCODE_S),
+                KeyEvent.META_META_ON or KeyEvent.META_ALT_ON,
+                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+            ),
         )
     }
 
diff --git a/tests/UsbManagerTests/lib/src/com/android/server/usblib/UsbManagerTestLib.java b/tests/UsbManagerTests/lib/src/com/android/server/usblib/UsbManagerTestLib.java
index e2099e6..635e5de 100644
--- a/tests/UsbManagerTests/lib/src/com/android/server/usblib/UsbManagerTestLib.java
+++ b/tests/UsbManagerTests/lib/src/com/android/server/usblib/UsbManagerTestLib.java
@@ -18,19 +18,27 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.hardware.usb.UsbAccessory;
 import android.hardware.usb.UsbManager;
+import android.hardware.usb.flags.Flags;
 import android.os.Binder;
+import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.util.Log;
 
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.util.concurrent.atomic.AtomicInteger;
 
 /**
@@ -43,13 +51,36 @@
 
     private UsbManager mUsbManagerSys;
     private UsbManager mUsbManagerMock;
-    @Mock private android.hardware.usb.IUsbManager mMockUsbService;
+    @Mock
+    private android.hardware.usb.IUsbManager mMockUsbService;
+    private TestParcelFileDescriptor mTestParcelFileDescriptor = new TestParcelFileDescriptor(
+            new ParcelFileDescriptor(new FileDescriptor()));
+    @Mock
+    private UsbAccessory mMockUsbAccessory;
 
     /**
      * Counter for tracking UsbOperation operations.
      */
     private static final AtomicInteger sUsbOperationCount = new AtomicInteger();
 
+    private class TestParcelFileDescriptor extends ParcelFileDescriptor {
+
+        private final AtomicInteger mCloseCount = new AtomicInteger();
+
+        TestParcelFileDescriptor(ParcelFileDescriptor wrapped) {
+            super(wrapped);
+        }
+
+        @Override
+        public void close() {
+            int unused = mCloseCount.incrementAndGet();
+        }
+
+        public void clearCloseCount() {
+            mCloseCount.set(0);
+        }
+    }
+
     public UsbManagerTestLib(Context context) {
         MockitoAnnotations.initMocks(this);
         mContext = context;
@@ -74,6 +105,34 @@
         mUsbManagerSys.setCurrentFunctions(functions);
     }
 
+    private InputStream openAccessoryInputStream(UsbAccessory accessory) {
+        try {
+            when(mMockUsbService.openAccessory(accessory)).thenReturn(mTestParcelFileDescriptor);
+        } catch (RemoteException remEx) {
+            Log.w(TAG, "RemoteException");
+        }
+
+        if (Flags.enableAccessoryStreamApi()) {
+            return mUsbManagerMock.openAccessoryInputStream(accessory);
+        }
+
+        throw new UnsupportedOperationException("Stream APIs not available");
+    }
+
+    private OutputStream openAccessoryOutputStream(UsbAccessory accessory) {
+        try {
+            when(mMockUsbService.openAccessory(accessory)).thenReturn(mTestParcelFileDescriptor);
+        } catch (RemoteException remEx) {
+            Log.w(TAG, "RemoteException");
+        }
+
+        if (Flags.enableAccessoryStreamApi()) {
+            return mUsbManagerMock.openAccessoryOutputStream(accessory);
+        }
+
+        throw new UnsupportedOperationException("Stream APIs not available");
+    }
+
     private void testSetGetCurrentFunctions_Matched(long functions) {
         setCurrentFunctions(functions);
         assertEquals("CurrentFunctions mismatched: ", functions, getCurrentFunctions());
@@ -94,7 +153,7 @@
         try {
             setCurrentFunctions(functions);
 
-            verify(mMockUsbService).setCurrentFunctions(eq(functions), operationId);
+            verify(mMockUsbService).setCurrentFunctions(eq(functions), eq(operationId));
         } catch (RemoteException remEx) {
             Log.w(TAG, "RemoteException");
         }
@@ -118,7 +177,7 @@
         int operationId = sUsbOperationCount.incrementAndGet() + Binder.getCallingUid();
         setCurrentFunctions(functions);
 
-        verify(mMockUsbService).setCurrentFunctions(eq(functions), operationId);
+        verify(mMockUsbService).setCurrentFunctions(eq(functions), eq(operationId));
     }
 
     public void testGetCurrentFunctions_shouldMatched() {
@@ -138,4 +197,47 @@
         testSetCurrentFunctionsMock_Matched(UsbManager.FUNCTION_RNDIS);
         testSetCurrentFunctionsMock_Matched(UsbManager.FUNCTION_NCM);
     }
+
+    public void testParcelFileDescriptorClosedWhenAllOpenStreamsAreClosed() {
+        mTestParcelFileDescriptor.clearCloseCount();
+        try {
+            try (InputStream ignored = openAccessoryInputStream(mMockUsbAccessory)) {
+                //noinspection EmptyTryBlock
+                try (OutputStream ignored2 = openAccessoryOutputStream(mMockUsbAccessory)) {
+                    // do nothing
+                }
+            }
+
+            // ParcelFileDescriptor is closed only once.
+            assertEquals(mTestParcelFileDescriptor.mCloseCount.get(), 1);
+            mTestParcelFileDescriptor.clearCloseCount();
+        } catch (IOException e) {
+            // do nothing
+        }
+    }
+
+    public void testOnlyOneOpenInputStreamAllowed() {
+        try {
+            //noinspection EmptyTryBlock
+            try (InputStream ignored = openAccessoryInputStream(mMockUsbAccessory)) {
+                assertThrows(IllegalStateException.class,
+                        () -> openAccessoryInputStream(mMockUsbAccessory));
+            }
+        } catch (IOException e) {
+            // do nothing
+        }
+    }
+
+    public void testOnlyOneOpenOutputStreamAllowed() {
+        try {
+            //noinspection EmptyTryBlock
+            try (OutputStream ignored = openAccessoryOutputStream(mMockUsbAccessory)) {
+                assertThrows(IllegalStateException.class,
+                        () -> openAccessoryOutputStream(mMockUsbAccessory));
+            }
+        } catch (IOException e) {
+            // do nothing
+        }
+    }
+
 }
diff --git a/tests/UsbManagerTests/src/com/android/server/usbtest/UsbManagerApiTest.java b/tests/UsbManagerTests/src/com/android/server/usbtest/UsbManagerApiTest.java
index 8b21763..40fd0b4 100644
--- a/tests/UsbManagerTests/src/com/android/server/usbtest/UsbManagerApiTest.java
+++ b/tests/UsbManagerTests/src/com/android/server/usbtest/UsbManagerApiTest.java
@@ -18,17 +18,21 @@
 
 import android.content.Context;
 import android.hardware.usb.UsbManager;
+import android.hardware.usb.flags.Flags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import org.junit.Ignore;
+import com.android.server.usblib.UsbManagerTestLib;
+
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import com.android.server.usblib.UsbManagerTestLib;
-
 /**
  * Unit tests for {@link android.hardware.usb.UsbManager}.
  * Note: MUST claimed MANAGE_USB permission in Manifest
@@ -41,6 +45,9 @@
     private final UsbManagerTestLib mUsbManagerTestLib =
             new UsbManagerTestLib(mContext = InstrumentationRegistry.getContext());
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
     /**
      * Verify NO SecurityException
      * Go through System Server
@@ -92,4 +99,23 @@
     public void testUsbApi_SetCurrentFunctions_shouldMatched() {
         mUsbManagerTestLib.testSetCurrentFunctions_shouldMatched();
     }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_ACCESSORY_STREAM_API)
+    public void testUsbApi_closesParcelFileDescriptorAfterAllStreamsClosed() {
+        mUsbManagerTestLib.testParcelFileDescriptorClosedWhenAllOpenStreamsAreClosed();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_ACCESSORY_STREAM_API)
+    public void testUsbApi_callingOpenAccessoryInputStreamTwiceThrowsException() {
+        mUsbManagerTestLib.testOnlyOneOpenInputStreamAllowed();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_ACCESSORY_STREAM_API)
+    public void testUsbApi_callingOpenAccessoryOutputStreamTwiceThrowsException() {
+        mUsbManagerTestLib.testOnlyOneOpenOutputStreamAllowed();
+    }
+
 }
diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java
index ac96ef2..be5c84c 100644
--- a/tests/testables/src/android/testing/TestableLooper.java
+++ b/tests/testables/src/android/testing/TestableLooper.java
@@ -53,7 +53,6 @@
     private static final Field MESSAGE_QUEUE_MESSAGES_FIELD;
     private static final Field MESSAGE_NEXT_FIELD;
     private static final Field MESSAGE_WHEN_FIELD;
-    private static Field MESSAGE_QUEUE_USE_CONCURRENT_FIELD = null;
 
     private Looper mLooper;
     private MessageQueue mQueue;
@@ -64,14 +63,6 @@
 
     static {
         try {
-            MESSAGE_QUEUE_USE_CONCURRENT_FIELD =
-                    MessageQueue.class.getDeclaredField("mUseConcurrent");
-            MESSAGE_QUEUE_USE_CONCURRENT_FIELD.setAccessible(true);
-        } catch (NoSuchFieldException ignored) {
-            // Ignore - maybe this is not CombinedMessageQueue?
-        }
-
-        try {
             MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages");
             MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true);
             MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next");
@@ -155,15 +146,6 @@
         mLooper = l;
         mQueue = mLooper.getQueue();
         mHandler = new Handler(mLooper);
-
-        // If we are using CombinedMessageQueue, we need to disable concurrent mode for testing.
-        if (MESSAGE_QUEUE_USE_CONCURRENT_FIELD != null) {
-            try {
-                MESSAGE_QUEUE_USE_CONCURRENT_FIELD.set(mQueue, false);
-            } catch (IllegalAccessException e) {
-                throw new RuntimeException(e);
-            }
-        }
     }
 
     /**
diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java
index 1bcfaf6..56b0a25 100644
--- a/tests/utils/testutils/java/android/os/test/TestLooper.java
+++ b/tests/utils/testutils/java/android/os/test/TestLooper.java
@@ -100,18 +100,6 @@
             throw new RuntimeException("Reflection error constructing or accessing looper", e);
         }
 
-        // If we are using CombinedMessageQueue, we need to disable concurrent mode for testing.
-        try {
-            Field messageQueueUseConcurrentField =
-                    MessageQueue.class.getDeclaredField("mUseConcurrent");
-            messageQueueUseConcurrentField.setAccessible(true);
-            messageQueueUseConcurrentField.set(mLooper.getQueue(), false);
-        } catch (NoSuchFieldException e) {
-            // Ignore - maybe this is not CombinedMessageQueue?
-        } catch (IllegalAccessException e) {
-            throw new RuntimeException("Reflection error constructing or accessing looper", e);
-        }
-
         mClock = clock;
     }