Merge "Update EMERGENCY_LOCATION op to default to ALLOWED" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 6ecd38f..3391698 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -335,6 +335,11 @@
     aconfig_declarations: "android.os.flags-aconfig",
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
     mode: "exported",
+    min_sdk_version: "30",
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.mediaprovider",
+    ],
 }
 
 cc_aconfig_library {
@@ -716,6 +721,7 @@
     name: "android.credentials.flags-aconfig",
     package: "android.credentials.flags",
     srcs: ["core/java/android/credentials/flags.aconfig"],
+    exportable: true,
 }
 
 java_aconfig_library {
@@ -724,6 +730,13 @@
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
 
+java_aconfig_library {
+    name: "android.credentials.flags-aconfig-java-export",
+    aconfig_declarations: "android.credentials.flags-aconfig",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+    mode: "exported",
+}
+
 // Content Protection
 aconfig_declarations {
     name: "android.view.contentprotection.flags-aconfig",
diff --git a/core/api/current.txt b/core/api/current.txt
index 4d3ca13..8a61f4a 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -10764,6 +10764,7 @@
     field public static final String OVERLAY_SERVICE = "overlay";
     field public static final String PEOPLE_SERVICE = "people";
     field public static final String PERFORMANCE_HINT_SERVICE = "performance_hint";
+    field @FlaggedApi("android.security.frp_enforcement") public static final String PERSISTENT_DATA_BLOCK_SERVICE = "persistent_data_block";
     field public static final String POWER_SERVICE = "power";
     field public static final String PRINT_SERVICE = "print";
     field @FlaggedApi("android.os.telemetry_apis_framework_initialization") public static final String PROFILING_SERVICE = "profiling";
@@ -20235,10 +20236,10 @@
     method public android.hardware.camera2.CaptureRequest getSessionParameters();
     method public int getSessionType();
     method public android.hardware.camera2.CameraCaptureSession.StateCallback getStateCallback();
-    method @FlaggedApi("com.android.internal.camera.flags.camera_device_setup") public void setCallback(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraCaptureSession.StateCallback);
     method public void setColorSpace(@NonNull android.graphics.ColorSpace.Named);
     method public void setInputConfiguration(@NonNull android.hardware.camera2.params.InputConfiguration);
     method public void setSessionParameters(android.hardware.camera2.CaptureRequest);
+    method @FlaggedApi("com.android.internal.camera.flags.camera_device_setup") public void setStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraCaptureSession.StateCallback);
     method public void writeToParcel(android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.hardware.camera2.params.SessionConfiguration> CREATOR;
     field public static final int SESSION_HIGH_SPEED = 1; // 0x1
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 8ceda62..0023e2a 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3797,7 +3797,6 @@
     field @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public static final String ON_DEVICE_INTELLIGENCE_SERVICE = "on_device_intelligence";
     field public static final String PERMISSION_CONTROLLER_SERVICE = "permission_controller";
     field public static final String PERMISSION_SERVICE = "permission";
-    field public static final String PERSISTENT_DATA_BLOCK_SERVICE = "persistent_data_block";
     field public static final String REBOOT_READINESS_SERVICE = "reboot_readiness";
     field public static final String ROLLBACK_SERVICE = "rollback";
     field public static final String SAFETY_CENTER_SERVICE = "safety_center";
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 1cbec31..66ec865 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -450,6 +450,11 @@
                 new CachedServiceFetcher<VcnManager>() {
             @Override
             public VcnManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+                if (!ctx.getPackageManager().hasSystemFeature(
+                        PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) {
+                    return null;
+                }
+
                 IBinder b = ServiceManager.getService(Context.VCN_MANAGEMENT_SERVICE);
                 IVcnManagementService service = IVcnManagementService.Stub.asInterface(b);
                 return new VcnManager(ctx, service);
@@ -1736,6 +1741,13 @@
         return fetcher;
     }
 
+    private static boolean hasSystemFeatureOpportunistic(@NonNull ContextImpl ctx,
+            @NonNull String featureName) {
+        PackageManager manager = ctx.getPackageManager();
+        if (manager == null) return true;
+        return manager.hasSystemFeature(featureName);
+    }
+
     /**
      * Gets a system service from a given context.
      * @hide
@@ -1758,12 +1770,18 @@
                 case Context.VIRTUALIZATION_SERVICE:
                 case Context.VIRTUAL_DEVICE_SERVICE:
                     return null;
-                case Context.SEARCH_SERVICE:
-                    // Wear device does not support SEARCH_SERVICE so we do not print WTF here
-                    PackageManager manager = ctx.getPackageManager();
-                    if (manager != null && manager.hasSystemFeature(PackageManager.FEATURE_WATCH)) {
+                case Context.VCN_MANAGEMENT_SERVICE:
+                    if (!hasSystemFeatureOpportunistic(ctx,
+                            PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) {
                         return null;
                     }
+                    break;
+                case Context.SEARCH_SERVICE:
+                    // Wear device does not support SEARCH_SERVICE so we do not print WTF here
+                    if (hasSystemFeatureOpportunistic(ctx, PackageManager.FEATURE_WATCH)) {
+                        return null;
+                    }
+                    break;
             }
             Slog.wtf(TAG, "Manager wrapper not available: " + name);
             return null;
diff --git a/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl b/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl
index 0dbe181..8bf288a 100644
--- a/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl
+++ b/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl
@@ -53,19 +53,22 @@
       void getFeatureDetails(in Feature feature, in IFeatureDetailsCallback featureDetailsCallback) = 4;
 
       @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)")
-      void requestFeatureDownload(in Feature feature, in ICancellationSignal signal, in IDownloadCallback callback) = 5;
+      void requestFeatureDownload(in Feature feature, in  AndroidFuture cancellationSignalFuture, in IDownloadCallback callback) = 5;
 
       @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)")
-      void requestTokenInfo(in Feature feature, in Bundle requestBundle, in  ICancellationSignal signal,
+      void requestTokenInfo(in Feature feature, in Bundle requestBundle, in  AndroidFuture cancellationSignalFuture,
                                                         in ITokenInfoCallback tokenInfocallback) = 6;
 
       @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)")
-      void processRequest(in Feature feature, in Bundle requestBundle, int requestType, in  ICancellationSignal cancellationSignal,
-                                                in IProcessingSignal signal, in IResponseCallback responseCallback) = 7;
+      void processRequest(in Feature feature, in Bundle requestBundle, int requestType,
+                                                in  AndroidFuture cancellationSignalFuture,
+                                                in AndroidFuture processingSignalFuture,
+                                                in IResponseCallback responseCallback) = 7;
 
       @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)")
       void processRequestStreaming(in Feature feature,
-                    in Bundle requestBundle, int requestType, in  ICancellationSignal cancellationSignal, in  IProcessingSignal signal,
+                    in Bundle requestBundle, int requestType, in  AndroidFuture cancellationSignalFuture,
+                    in  AndroidFuture processingSignalFuture,
                     in IStreamingResponseCallback streamingCallback) = 8;
 
       String getRemoteServicePackageName() = 9;
diff --git a/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java b/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java
index a465e3c..bc50d2e4 100644
--- a/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java
+++ b/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java
@@ -26,22 +26,23 @@
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
-import android.content.ComponentName;
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.CancellationSignal;
+import android.os.IBinder;
 import android.os.ICancellationSignal;
 import android.os.OutcomeReceiver;
 import android.os.PersistableBundle;
 import android.os.RemoteCallback;
 import android.os.RemoteException;
 import android.system.OsConstants;
+import android.util.Log;
 
 import androidx.annotation.IntDef;
 
-import com.android.internal.R;
+import com.android.internal.infra.AndroidFuture;
 
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
@@ -76,6 +77,8 @@
      */
     public static final String AUGMENT_REQUEST_CONTENT_BUNDLE_KEY =
             "AugmentRequestContentBundleKey";
+
+    private static final String TAG = "OnDeviceIntelligence";
     private final Context mContext;
     private final IOnDeviceIntelligenceManager mService;
 
@@ -121,9 +124,9 @@
     @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
     public String getRemoteServicePackageName() {
         String result;
-        try{
-           result = mService.getRemoteServicePackageName();
-        } catch (RemoteException e){
+        try {
+            result = mService.getRemoteServicePackageName();
+        } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
         return result;
@@ -288,18 +291,15 @@
                 }
             };
 
-            ICancellationSignal transport = null;
-            if (cancellationSignal != null) {
-                transport = CancellationSignal.createTransport();
-                cancellationSignal.setRemote(transport);
-            }
-
-            mService.requestFeatureDownload(feature, transport, downloadCallback);
+            mService.requestFeatureDownload(feature,
+                    configureRemoteCancellationFuture(cancellationSignal, callbackExecutor),
+                    downloadCallback);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
     }
 
+
     /**
      * The methods computes the token related information for a given request payload using the
      * provided {@link Feature}.
@@ -337,13 +337,9 @@
                 }
             };
 
-            ICancellationSignal transport = null;
-            if (cancellationSignal != null) {
-                transport = CancellationSignal.createTransport();
-                cancellationSignal.setRemote(transport);
-            }
-
-            mService.requestTokenInfo(feature, request, transport, callback);
+            mService.requestTokenInfo(feature, request,
+                    configureRemoteCancellationFuture(cancellationSignal, callbackExecutor),
+                    callback);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -407,19 +403,9 @@
             };
 
 
-            IProcessingSignal transport = null;
-            if (processingSignal != null) {
-                transport = ProcessingSignal.createTransport();
-                processingSignal.setRemote(transport);
-            }
-
-            ICancellationSignal cancellationTransport = null;
-            if (cancellationSignal != null) {
-                cancellationTransport = CancellationSignal.createTransport();
-                cancellationSignal.setRemote(cancellationTransport);
-            }
-
-            mService.processRequest(feature, request, requestType, cancellationTransport, transport,
+            mService.processRequest(feature, request, requestType,
+                    configureRemoteCancellationFuture(cancellationSignal, callbackExecutor),
+                    configureRemoteProcessingSignalFuture(processingSignal, callbackExecutor),
                     callback);
 
         } catch (RemoteException e) {
@@ -449,7 +435,8 @@
      * @param callbackExecutor          executor to run the callback on.
      */
     @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)
-    public void processRequestStreaming(@NonNull Feature feature, @NonNull @InferenceParams Bundle request,
+    public void processRequestStreaming(@NonNull Feature feature,
+            @NonNull @InferenceParams Bundle request,
             @RequestType int requestType,
             @Nullable CancellationSignal cancellationSignal,
             @Nullable ProcessingSignal processingSignal,
@@ -500,20 +487,11 @@
                 }
             };
 
-            IProcessingSignal transport = null;
-            if (processingSignal != null) {
-                transport = ProcessingSignal.createTransport();
-                processingSignal.setRemote(transport);
-            }
-
-            ICancellationSignal cancellationTransport = null;
-            if (cancellationSignal != null) {
-                cancellationTransport = CancellationSignal.createTransport();
-                cancellationSignal.setRemote(cancellationTransport);
-            }
-
             mService.processRequestStreaming(
-                    feature, request, requestType, cancellationTransport, transport, callback);
+                    feature, request, requestType,
+                    configureRemoteCancellationFuture(cancellationSignal, callbackExecutor),
+                    configureRemoteProcessingSignalFuture(processingSignal, callbackExecutor),
+                    callback);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -574,4 +552,45 @@
     @Target({ElementType.PARAMETER, ElementType.FIELD})
     public @interface InferenceParams {
     }
+
+
+    @Nullable
+    private static AndroidFuture<IBinder> configureRemoteCancellationFuture(
+            @Nullable CancellationSignal cancellationSignal,
+            @NonNull Executor callbackExecutor) {
+        if (cancellationSignal == null) {
+            return null;
+        }
+        AndroidFuture<IBinder> cancellationFuture = new AndroidFuture<>();
+        cancellationFuture.whenCompleteAsync(
+                (cancellationTransport, error) -> {
+                    if (error != null || cancellationTransport == null) {
+                        Log.e(TAG, "Unable to receive the remote cancellation signal.", error);
+                    } else {
+                        cancellationSignal.setRemote(
+                                ICancellationSignal.Stub.asInterface(cancellationTransport));
+                    }
+                }, callbackExecutor);
+        return cancellationFuture;
+    }
+
+    @Nullable
+    private static AndroidFuture<IBinder> configureRemoteProcessingSignalFuture(
+            ProcessingSignal processingSignal, Executor executor) {
+        if (processingSignal == null) {
+            return null;
+        }
+        AndroidFuture<IBinder> processingSignalFuture = new AndroidFuture<>();
+        processingSignalFuture.whenCompleteAsync(
+                (transport, error) -> {
+                    if (error != null || transport == null) {
+                        Log.e(TAG, "Unable to receive the remote processing signal.", error);
+                    } else {
+                        processingSignal.setRemote(IProcessingSignal.Stub.asInterface(transport));
+                    }
+                }, executor);
+        return processingSignalFuture;
+    }
+
+
 }
diff --git a/core/java/android/app/ondeviceintelligence/ProcessingSignal.java b/core/java/android/app/ondeviceintelligence/ProcessingSignal.java
index c275cc7..733f4fa 100644
--- a/core/java/android/app/ondeviceintelligence/ProcessingSignal.java
+++ b/core/java/android/app/ondeviceintelligence/ProcessingSignal.java
@@ -123,10 +123,10 @@
      * Sets the processing signal callback to be called when signals are received.
      *
      * This method is intended to be used by the recipient of a processing signal
-     * such as the remote implementation for {@link OnDeviceIntelligenceManager} to handle
-     * cancellation requests while performing a long-running operation.  This method is not
-     * intended
-     * to be used by applications themselves.
+     * such as the remote implementation in
+     * {@link android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService} to handle
+     * processing signals while performing a long-running operation.  This method is not
+     * intended to be used by the caller themselves.
      *
      * If {@link ProcessingSignal#sendSignal} has already been called, then the provided callback
      * is invoked immediately and all previously queued actions are passed to remote signal.
@@ -200,7 +200,7 @@
     }
 
     /**
-     * Given a locally created transport, returns its associated cancellation signal.
+     * Given a locally created transport, returns its associated processing signal.
      *
      * @param transport The locally created transport, or null if none.
      * @return The associated processing signal, or null if none.
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index 3304475..ec59cf6 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -972,6 +972,7 @@
          *
          * @param config camera configuration.
          * @return newly created camera.
+         * @throws UnsupportedOperationException if virtual camera isn't supported on this device.
          * @see VirtualDeviceParams#POLICY_TYPE_CAMERA
          */
         @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 89300e3..284e318 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -4208,7 +4208,7 @@
             MEDIA_COMMUNICATION_SERVICE,
             BATTERY_SERVICE,
             JOB_SCHEDULER_SERVICE,
-            //@hide: PERSISTENT_DATA_BLOCK_SERVICE,
+            PERSISTENT_DATA_BLOCK_SERVICE,
             //@hide: OEM_LOCK_SERVICE,
             MEDIA_PROJECTION_SERVICE,
             MIDI_SERVICE,
@@ -5930,9 +5930,8 @@
      *
      * @see #getSystemService(String)
      * @see android.service.persistentdata.PersistentDataBlockManager
-     * @hide
      */
-    @SystemApi
+    @FlaggedApi(android.security.Flags.FLAG_FRP_ENFORCEMENT)
     public static final String PERSISTENT_DATA_BLOCK_SERVICE = "persistent_data_block";
 
     /**
diff --git a/core/java/android/credentials/flags.aconfig b/core/java/android/credentials/flags.aconfig
index 47edba6..16ca31f 100644
--- a/core/java/android/credentials/flags.aconfig
+++ b/core/java/android/credentials/flags.aconfig
@@ -47,6 +47,7 @@
     name: "configurable_selector_ui_enabled"
     description: "Enables OEM configurable Credential Selector UI"
     bug: "319448437"
+    is_exported: true
 }
 
 flag {
diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java
index 13d5c7e..6f901d7 100644
--- a/core/java/android/hardware/camera2/CaptureRequest.java
+++ b/core/java/android/hardware/camera2/CaptureRequest.java
@@ -2800,7 +2800,9 @@
      * upright.</p>
      * <p>Camera devices may either encode this value into the JPEG EXIF header, or
      * rotate the image data to match this orientation. When the image data is rotated,
-     * the thumbnail data will also be rotated.</p>
+     * the thumbnail data will also be rotated. Additionally, in the case where the image data
+     * is rotated, {@link android.media.Image#getWidth } and {@link android.media.Image#getHeight }
+     * will not be updated to reflect the height and width of the rotated image.</p>
      * <p>Note that this orientation is relative to the orientation of the camera sensor, given
      * by {@link CameraCharacteristics#SENSOR_ORIENTATION android.sensor.orientation}.</p>
      * <p>To translate from the device orientation given by the Android sensor APIs for camera
diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java
index 7145501..69b1c34 100644
--- a/core/java/android/hardware/camera2/CaptureResult.java
+++ b/core/java/android/hardware/camera2/CaptureResult.java
@@ -3091,7 +3091,9 @@
      * upright.</p>
      * <p>Camera devices may either encode this value into the JPEG EXIF header, or
      * rotate the image data to match this orientation. When the image data is rotated,
-     * the thumbnail data will also be rotated.</p>
+     * the thumbnail data will also be rotated. Additionally, in the case where the image data
+     * is rotated, {@link android.media.Image#getWidth } and {@link android.media.Image#getHeight }
+     * will not be updated to reflect the height and width of the rotated image.</p>
      * <p>Note that this orientation is relative to the orientation of the camera sensor, given
      * by {@link CameraCharacteristics#SENSOR_ORIENTATION android.sensor.orientation}.</p>
      * <p>To translate from the device orientation given by the Android sensor APIs for camera
diff --git a/core/java/android/hardware/camera2/params/SessionConfiguration.java b/core/java/android/hardware/camera2/params/SessionConfiguration.java
index b0f354f..3b2913c 100644
--- a/core/java/android/hardware/camera2/params/SessionConfiguration.java
+++ b/core/java/android/hardware/camera2/params/SessionConfiguration.java
@@ -133,7 +133,7 @@
      * {@link CameraDeviceSetup.isSessionConfigurationSupported} and {@link
      * CameraDeviceSetup.getSessionCharacteristics} to query a camera device's feature
      * combination support and session specific characteristics. For the SessionConfiguration
-     * object to be used to create a capture session, {@link #setCallback} must be called to
+     * object to be used to create a capture session, {@link #setStateCallback} must be called to
      * specify the state callback function, and any incomplete OutputConfigurations must be
      * completed via {@link OutputConfiguration#addSurface} or
      * {@link OutputConfiguration#setSurfacesForMultiResolutionOutput} as appropriate.</p>
@@ -419,7 +419,7 @@
      * @param cb A state callback interface implementation.
      */
     @FlaggedApi(Flags.FLAG_CAMERA_DEVICE_SETUP)
-    public void setCallback(
+    public void setStateCallback(
             @NonNull @CallbackExecutor Executor executor,
             @NonNull CameraCaptureSession.StateCallback cb) {
         mStateCallback = cb;
diff --git a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java
index b067095..978a8f9 100644
--- a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java
+++ b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java
@@ -1473,6 +1473,11 @@
      * <li>ImageFormat.DEPTH_JPEG => HAL_DATASPACE_DYNAMIC_DEPTH
      * <li>ImageFormat.HEIC => HAL_DATASPACE_HEIF
      * <li>ImageFormat.JPEG_R => HAL_DATASPACE_JPEG_R
+     * <li>ImageFormat.YUV_420_888 => HAL_DATASPACE_JFIF
+     * <li>ImageFormat.RAW_SENSOR => HAL_DATASPACE_ARBITRARY
+     * <li>ImageFormat.RAW_OPAQUE => HAL_DATASPACE_ARBITRARY
+     * <li>ImageFormat.RAW10 => HAL_DATASPACE_ARBITRARY
+     * <li>ImageFormat.RAW12 => HAL_DATASPACE_ARBITRARY
      * <li>others => HAL_DATASPACE_UNKNOWN
      * </ul>
      * </p>
@@ -1511,6 +1516,11 @@
                 return HAL_DATASPACE_JPEG_R;
             case ImageFormat.YUV_420_888:
                 return HAL_DATASPACE_JFIF;
+            case ImageFormat.RAW_SENSOR:
+            case ImageFormat.RAW_PRIVATE:
+            case ImageFormat.RAW10:
+            case ImageFormat.RAW12:
+                return HAL_DATASPACE_ARBITRARY;
             default:
                 return HAL_DATASPACE_UNKNOWN;
         }
@@ -2005,6 +2015,12 @@
     private static final int HAL_DATASPACE_RANGE_SHIFT = 27;
 
     private static final int HAL_DATASPACE_UNKNOWN = 0x0;
+
+    /**
+     * @hide
+     */
+    public static final int HAL_DATASPACE_ARBITRARY = 0x1;
+
     /** @hide */
     public static final int HAL_DATASPACE_V0_JFIF =
             (2 << HAL_DATASPACE_STANDARD_SHIFT) |
diff --git a/core/java/android/hardware/devicestate/DeviceState.java b/core/java/android/hardware/devicestate/DeviceState.java
index b214da2..689e343 100644
--- a/core/java/android/hardware/devicestate/DeviceState.java
+++ b/core/java/android/hardware/devicestate/DeviceState.java
@@ -173,7 +173,7 @@
     public static final int PROPERTY_FEATURE_DUAL_DISPLAY_INTERNAL_DEFAULT = 17;
 
     /** @hide */
-    @IntDef(prefix = {"PROPERTY_"}, flag = true, value = {
+    @IntDef(prefix = {"PROPERTY_"}, flag = false, value = {
             PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED,
             PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_HALF_OPEN,
             PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_OPEN,
@@ -197,7 +197,7 @@
     public @interface DeviceStateProperties {}
 
     /** @hide */
-    @IntDef(prefix = {"PROPERTY_"}, flag = true, value = {
+    @IntDef(prefix = {"PROPERTY_"}, flag = false, value = {
             PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED,
             PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_HALF_OPEN,
             PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_OPEN
@@ -207,7 +207,7 @@
     public @interface PhysicalDeviceStateProperties {}
 
     /** @hide */
-    @IntDef(prefix = {"PROPERTY_"}, flag = true, value = {
+    @IntDef(prefix = {"PROPERTY_"}, flag = false, value = {
             PROPERTY_POLICY_CANCEL_OVERRIDE_REQUESTS,
             PROPERTY_POLICY_CANCEL_WHEN_REQUESTER_NOT_ON_TOP,
             PROPERTY_POLICY_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL,
diff --git a/core/java/android/os/Trace.java b/core/java/android/os/Trace.java
index bebb912..edb3a64 100644
--- a/core/java/android/os/Trace.java
+++ b/core/java/android/os/Trace.java
@@ -125,15 +125,15 @@
     @UnsupportedAppUsage
     @CriticalNative
     @android.ravenwood.annotation.RavenwoodReplace
-    private static native long nativeGetEnabledTags();
+    private static native boolean nativeIsTagEnabled(long tag);
     @android.ravenwood.annotation.RavenwoodReplace
     private static native void nativeSetAppTracingAllowed(boolean allowed);
     @android.ravenwood.annotation.RavenwoodReplace
     private static native void nativeSetTracingEnabled(boolean allowed);
 
-    private static long nativeGetEnabledTags$ravenwood() {
+    private static boolean nativeIsTagEnabled$ravenwood(long traceTag) {
         // Tracing currently completely disabled under Ravenwood
-        return 0;
+        return false;
     }
 
     private static void nativeSetAppTracingAllowed$ravenwood(boolean allowed) {
@@ -181,8 +181,7 @@
     @UnsupportedAppUsage
     @SystemApi(client = MODULE_LIBRARIES)
     public static boolean isTagEnabled(long traceTag) {
-        long tags = nativeGetEnabledTags();
-        return (tags & traceTag) != 0;
+        return nativeIsTagEnabled(traceTag);
     }
 
     /**
diff --git a/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl b/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl
index 6dbff71..908ab5f 100644
--- a/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl
+++ b/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl
@@ -41,7 +41,9 @@
     void getFeatureDetails(int callerUid, in Feature feature, in IFeatureDetailsCallback featureDetailsCallback);
     void getReadOnlyFileDescriptor(in String fileName, in AndroidFuture<ParcelFileDescriptor> future);
     void getReadOnlyFeatureFileDescriptorMap(in Feature feature, in RemoteCallback remoteCallback);
-    void requestFeatureDownload(int callerUid, in Feature feature, in ICancellationSignal cancellationSignal, in IDownloadCallback downloadCallback);
+    void requestFeatureDownload(int callerUid, in Feature feature,
+                                in AndroidFuture<ICancellationSignal> cancellationSignal,
+                                in IDownloadCallback downloadCallback);
     void registerRemoteServices(in IRemoteProcessingService remoteProcessingService);
     void notifyInferenceServiceConnected();
     void notifyInferenceServiceDisconnected();
diff --git a/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl b/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl
index 799c7545..4213a09 100644
--- a/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl
+++ b/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl
@@ -24,6 +24,7 @@
 import android.os.ICancellationSignal;
 import android.os.PersistableBundle;
 import android.os.Bundle;
+import com.android.internal.infra.AndroidFuture;
 import android.service.ondeviceintelligence.IRemoteStorageService;
 import android.service.ondeviceintelligence.IProcessingUpdateStatusCallback;
 
@@ -34,13 +35,16 @@
  */
 oneway interface IOnDeviceSandboxedInferenceService {
     void registerRemoteStorageService(in IRemoteStorageService storageService);
-    void requestTokenInfo(int callerUid, in Feature feature, in Bundle request, in ICancellationSignal cancellationSignal,
+    void requestTokenInfo(int callerUid, in Feature feature, in Bundle request,
+                            in AndroidFuture<ICancellationSignal> cancellationSignal,
                             in ITokenInfoCallback tokenInfoCallback);
     void processRequest(int callerUid, in Feature feature, in Bundle request, in int requestType,
-                        in ICancellationSignal cancellationSignal, in IProcessingSignal processingSignal,
+                        in AndroidFuture<ICancellationSignal> cancellationSignal,
+                        in AndroidFuture<IProcessingSignal> processingSignal,
                         in IResponseCallback callback);
     void processRequestStreaming(int callerUid, in Feature feature, in Bundle request, in int requestType,
-                                in ICancellationSignal cancellationSignal, in IProcessingSignal processingSignal,
+                                in AndroidFuture<ICancellationSignal> cancellationSignal,
+                                in AndroidFuture<IProcessingSignal> processingSignal,
                                 in IStreamingResponseCallback callback);
     void updateProcessingState(in Bundle processingState,
                                      in IProcessingUpdateStatusCallback callback);
diff --git a/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java b/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java
index 9321318..86320b8 100644
--- a/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java
+++ b/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java
@@ -148,14 +148,18 @@
 
                 @Override
                 public void requestFeatureDownload(int callerUid, Feature feature,
-                        ICancellationSignal cancellationSignal,
+                        AndroidFuture cancellationSignalFuture,
                         IDownloadCallback downloadCallback) {
                     Objects.requireNonNull(feature);
                     Objects.requireNonNull(downloadCallback);
-
+                    ICancellationSignal transport = null;
+                    if (cancellationSignalFuture != null) {
+                        transport = CancellationSignal.createTransport();
+                        cancellationSignalFuture.complete(transport);
+                    }
                     OnDeviceIntelligenceService.this.onDownloadFeature(callerUid,
                             feature,
-                            CancellationSignal.fromTransport(cancellationSignal),
+                            CancellationSignal.fromTransport(transport),
                             wrapDownloadCallback(downloadCallback));
                 }
 
diff --git a/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java b/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java
index fc7a4c8..96c45ee 100644
--- a/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java
+++ b/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java
@@ -122,46 +122,72 @@
 
                 @Override
                 public void requestTokenInfo(int callerUid, Feature feature, Bundle request,
-                        ICancellationSignal cancellationSignal,
+                        AndroidFuture cancellationSignalFuture,
                         ITokenInfoCallback tokenInfoCallback) {
                     Objects.requireNonNull(feature);
                     Objects.requireNonNull(tokenInfoCallback);
+                    ICancellationSignal transport = null;
+                    if (cancellationSignalFuture != null) {
+                        transport = CancellationSignal.createTransport();
+                        cancellationSignalFuture.complete(transport);
+                    }
                     OnDeviceSandboxedInferenceService.this.onTokenInfoRequest(callerUid,
                             feature,
                             request,
-                            CancellationSignal.fromTransport(cancellationSignal),
+                            CancellationSignal.fromTransport(transport),
                             wrapTokenInfoCallback(tokenInfoCallback));
                 }
 
                 @Override
                 public void processRequestStreaming(int callerUid, Feature feature, Bundle request,
-                        int requestType, ICancellationSignal cancellationSignal,
-                        IProcessingSignal processingSignal,
+                        int requestType,
+                        AndroidFuture cancellationSignalFuture,
+                        AndroidFuture processingSignalFuture,
                         IStreamingResponseCallback callback) {
                     Objects.requireNonNull(feature);
                     Objects.requireNonNull(callback);
 
+                    ICancellationSignal transport = null;
+                    if (cancellationSignalFuture != null) {
+                        transport = CancellationSignal.createTransport();
+                        cancellationSignalFuture.complete(transport);
+                    }
+                    IProcessingSignal processingSignalTransport = null;
+                    if (processingSignalFuture != null) {
+                        processingSignalTransport = ProcessingSignal.createTransport();
+                        processingSignalFuture.complete(processingSignalTransport);
+                    }
                     OnDeviceSandboxedInferenceService.this.onProcessRequestStreaming(callerUid,
                             feature,
                             request,
                             requestType,
-                            CancellationSignal.fromTransport(cancellationSignal),
-                            ProcessingSignal.fromTransport(processingSignal),
+                            CancellationSignal.fromTransport(transport),
+                            ProcessingSignal.fromTransport(processingSignalTransport),
                             wrapStreamingResponseCallback(callback));
                 }
 
                 @Override
                 public void processRequest(int callerUid, Feature feature, Bundle request,
-                        int requestType, ICancellationSignal cancellationSignal,
-                        IProcessingSignal processingSignal,
+                        int requestType,
+                        AndroidFuture cancellationSignalFuture,
+                        AndroidFuture processingSignalFuture,
                         IResponseCallback callback) {
                     Objects.requireNonNull(feature);
                     Objects.requireNonNull(callback);
-
+                    ICancellationSignal transport = null;
+                    if (cancellationSignalFuture != null) {
+                        transport = CancellationSignal.createTransport();
+                        cancellationSignalFuture.complete(transport);
+                    }
+                    IProcessingSignal processingSignalTransport = null;
+                    if (processingSignalFuture != null) {
+                        processingSignalTransport = ProcessingSignal.createTransport();
+                        processingSignalFuture.complete(processingSignalTransport);
+                    }
                     OnDeviceSandboxedInferenceService.this.onProcessRequest(callerUid, feature,
                             request, requestType,
-                            CancellationSignal.fromTransport(cancellationSignal),
-                            ProcessingSignal.fromTransport(processingSignal),
+                            CancellationSignal.fromTransport(transport),
+                            ProcessingSignal.fromTransport(processingSignalTransport),
                             wrapResponseCallback(callback));
                 }
 
@@ -206,7 +232,8 @@
      * Invoked when caller provides a request for a particular feature to be processed in a
      * streaming manner. The expectation from the implementation is that when processing the
      * request,
-     * it periodically populates the {@link StreamingProcessingCallback#onPartialResult} to continuously
+     * it periodically populates the {@link StreamingProcessingCallback#onPartialResult} to
+     * continuously
      * provide partial Bundle results for the caller to utilize. Optionally the implementation can
      * provide the complete response in the {@link StreamingProcessingCallback#onResult} upon
      * processing completion.
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index aff1d4a..5366a4d 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -126,3 +126,10 @@
   description: "When handwriting is initiated in an unfocused TextView, cursor is placed at the end of the closest paragraph."
   bug: "323376217"
 }
+
+flag {
+  name: "handwriting_unsupported_message"
+  namespace: "text"
+  description: "Feature flag for showing error message when user tries stylus handwriting on a text field which doesn't support it"
+  bug: "297962571"
+}
diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java
index 29c8350..192b2ec 100644
--- a/core/java/android/view/HandwritingInitiator.java
+++ b/core/java/android/view/HandwritingInitiator.java
@@ -34,7 +34,9 @@
 import android.widget.EditText;
 import android.widget.Editor;
 import android.widget.TextView;
+import android.widget.Toast;
 
+import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.lang.ref.WeakReference;
@@ -223,7 +225,24 @@
                     View candidateView = findBestCandidateView(mState.mStylusDownX,
                             mState.mStylusDownY, /* isHover */ false);
                     if (candidateView != null && candidateView.isEnabled()) {
-                        if (candidateView == getConnectedOrFocusedView()) {
+                        if (shouldShowHandwritingUnavailableMessageForView(candidateView)) {
+                            int messagesResId = (candidateView instanceof TextView tv
+                                    && tv.isAnyPasswordInputType())
+                                    ? R.string.error_handwriting_unsupported_password
+                                    : R.string.error_handwriting_unsupported;
+                            Toast.makeText(candidateView.getContext(), messagesResId,
+                                    Toast.LENGTH_SHORT).show();
+                            if (!candidateView.hasFocus()) {
+                                requestFocusWithoutReveal(candidateView);
+                            }
+                            mImm.showSoftInput(candidateView, 0);
+                            mState.mHandled = true;
+                            mState.mShouldInitHandwriting = false;
+                            motionEvent.setAction((motionEvent.getAction()
+                                    & MotionEvent.ACTION_POINTER_INDEX_MASK)
+                                    | MotionEvent.ACTION_CANCEL);
+                            candidateView.getRootView().dispatchTouchEvent(motionEvent);
+                        } else if (candidateView == getConnectedOrFocusedView()) {
                             if (!mInitiateWithoutConnection && !candidateView.hasFocus()) {
                                 requestFocusWithoutReveal(candidateView);
                             }
@@ -484,6 +503,15 @@
         return view.isStylusHandwritingAvailable();
     }
 
+    private static boolean shouldShowHandwritingUnavailableMessageForView(@NonNull View view) {
+        return (view instanceof TextView) && !shouldTriggerStylusHandwritingForView(view);
+    }
+
+    private static boolean shouldTriggerHandwritingOrShowUnavailableMessageForView(
+            @NonNull View view) {
+        return (view instanceof TextView) || shouldTriggerStylusHandwritingForView(view);
+    }
+
     /**
      * Returns the pointer icon for the motion event, or null if it doesn't specify the icon.
      * This gives HandwritingInitiator a chance to show the stylus handwriting icon over a
@@ -491,7 +519,7 @@
      */
     public PointerIcon onResolvePointerIcon(Context context, MotionEvent event) {
         final View hoverView = findHoverView(event);
-        if (hoverView == null) {
+        if (hoverView == null || !shouldTriggerStylusHandwritingForView(hoverView)) {
             return null;
         }
 
@@ -594,7 +622,7 @@
 
     /**
      * Given the location of the stylus event, return the best candidate view to initialize
-     * handwriting mode.
+     * handwriting mode or show the handwriting unavailable error message.
      *
      * @param x the x coordinates of the stylus event, in the coordinates of the window.
      * @param y the y coordinates of the stylus event, in the coordinates of the window.
@@ -610,7 +638,8 @@
             Rect handwritingArea = mTempRect;
             if (getViewHandwritingArea(connectedOrFocusedView, handwritingArea)
                     && isInHandwritingArea(handwritingArea, x, y, connectedOrFocusedView, isHover)
-                    && shouldTriggerStylusHandwritingForView(connectedOrFocusedView)) {
+                    && shouldTriggerHandwritingOrShowUnavailableMessageForView(
+                            connectedOrFocusedView)) {
                 if (!isHover && mState != null) {
                     mState.mStylusDownWithinEditorBounds =
                             contains(handwritingArea, x, y, 0f, 0f, 0f, 0f);
@@ -628,7 +657,7 @@
             final View view = viewInfo.getView();
             final Rect handwritingArea = viewInfo.getHandwritingArea();
             if (!isInHandwritingArea(handwritingArea, x, y, view, isHover)
-                    || !shouldTriggerStylusHandwritingForView(view)) {
+                    || !shouldTriggerHandwritingOrShowUnavailableMessageForView(view)) {
                 continue;
             }
 
@@ -856,7 +885,7 @@
     /** The helper method to check if the given view is still active for handwriting. */
     private static boolean isViewActive(@Nullable View view) {
         return view != null && view.isAttachedToWindow() && view.isAggregatedVisible()
-                && view.shouldInitiateHandwriting();
+                && view.shouldTrackHandwritingArea();
     }
 
     private CursorAnchorInfo getCursorAnchorInfoForConnectionless(View view) {
diff --git a/core/java/android/view/OWNERS b/core/java/android/view/OWNERS
index a2f767d..07d05a4 100644
--- a/core/java/android/view/OWNERS
+++ b/core/java/android/view/OWNERS
@@ -75,12 +75,14 @@
 per-file View.java = file:/services/core/java/com/android/server/input/OWNERS
 per-file View.java = file:/services/core/java/com/android/server/wm/OWNERS
 per-file View.java = file:/core/java/android/view/inputmethod/OWNERS
+per-file View.java = file:/core/java/android/text/OWNERS
 per-file ViewRootImpl.java = file:/services/accessibility/OWNERS
 per-file ViewRootImpl.java = file:/core/java/android/service/autofill/OWNERS
 per-file ViewRootImpl.java = file:/graphics/java/android/graphics/OWNERS
 per-file ViewRootImpl.java = file:/services/core/java/com/android/server/input/OWNERS
 per-file ViewRootImpl.java = file:/services/core/java/com/android/server/wm/OWNERS
 per-file ViewRootImpl.java = file:/core/java/android/view/inputmethod/OWNERS
+per-file ViewRootImpl.java = file:/core/java/android/text/OWNERS
 per-file AccessibilityInteractionController.java = file:/services/accessibility/OWNERS
 per-file OnReceiveContentListener.java = file:/core/java/android/service/autofill/OWNERS
 per-file OnReceiveContentListener.java = file:/core/java/android/widget/OWNERS
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 0a75f4e..41bfb24 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -12695,7 +12695,7 @@
         if (getSystemGestureExclusionRects().isEmpty()
                 && collectPreferKeepClearRects().isEmpty()
                 && collectUnrestrictedPreferKeepClearRects().isEmpty()
-                && (info.mHandwritingArea == null || !shouldInitiateHandwriting())) {
+                && (info.mHandwritingArea == null || !shouldTrackHandwritingArea())) {
             if (info.mPositionUpdateListener != null) {
                 mRenderNode.removePositionUpdateListener(info.mPositionUpdateListener);
                 info.mPositionUpdateListener = null;
@@ -13062,7 +13062,7 @@
 
     void updateHandwritingArea() {
         // If autoHandwritingArea is not enabled, do nothing.
-        if (!shouldInitiateHandwriting()) return;
+        if (!shouldTrackHandwritingArea()) return;
         final AttachInfo ai = mAttachInfo;
         if (ai != null) {
             ai.mViewRootImpl.getHandwritingInitiator().updateHandwritingAreasForView(this);
@@ -13080,6 +13080,16 @@
     }
 
     /**
+     * Returns whether the handwriting initiator should track the handwriting area for this view,
+     * either to initiate handwriting mode, or to prepare handwriting delegation, or to show the
+     * handwriting unsupported message.
+     * @hide
+     */
+    public boolean shouldTrackHandwritingArea() {
+        return shouldInitiateHandwriting();
+    }
+
+    /**
      * Sets a callback which should be called when a stylus {@link MotionEvent} occurs within this
      * view's bounds. The callback will be called from the UI thread.
      *
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 0373539..5260470 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -13565,6 +13565,15 @@
 
     /** @hide */
     @Override
+    public boolean shouldTrackHandwritingArea() {
+        // The handwriting initiator tracks all editable TextViews regardless of whether handwriting
+        // is supported, so that it can show an error message for unsupported editable TextViews.
+        return super.shouldTrackHandwritingArea()
+                || (Flags.handwritingUnsupportedMessage() && onCheckIsTextEditor());
+    }
+
+    /** @hide */
+    @Override
     public boolean isStylusHandwritingAvailable() {
         if (mTextOperationUser == null) {
             return super.isStylusHandwritingAvailable();
diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp
index 5223798..d48cdc4 100644
--- a/core/jni/android_media_AudioSystem.cpp
+++ b/core/jni/android_media_AudioSystem.cpp
@@ -161,6 +161,7 @@
     jfieldID mMixType;
     jfieldID mCallbackFlags;
     jfieldID mToken;
+    jfieldID mVirtualDeviceId;
 } gAudioMixFields;
 
 static jclass gAudioFormatClass;
@@ -2312,7 +2313,7 @@
     jstring deviceAddress = env->NewStringUTF(nAudioMix.mDeviceAddress.c_str());
     *jAudioMix = env->NewObject(gAudioMixClass, gAudioMixCstor, jAudioMixingRule, jAudioFormat,
                                 nAudioMix.mRouteFlags, nAudioMix.mCbFlags, nAudioMix.mDeviceType,
-                                deviceAddress, jBinderToken);
+                                deviceAddress, jBinderToken, nAudioMix.mVirtualDeviceId);
     return AUDIO_JAVA_SUCCESS;
 }
 
@@ -2347,6 +2348,7 @@
             aiBinder(AIBinder_fromJavaBinder(env, jToken), &AIBinder_decStrong);
     nAudioMix->mToken = AIBinder_toPlatformBinder(aiBinder.get());
 
+    nAudioMix->mVirtualDeviceId = env->GetIntField(jAudioMix, gAudioMixFields.mVirtualDeviceId);
     jint status = convertAudioMixingRuleToNative(env, jRule, &(nAudioMix->mCriteria));
 
     env->DeleteLocalRef(jRule);
@@ -3676,7 +3678,7 @@
         gAudioMixCstor =
                 GetMethodIDOrDie(env, audioMixClass, "<init>",
                                  "(Landroid/media/audiopolicy/AudioMixingRule;Landroid/"
-                                 "media/AudioFormat;IIILjava/lang/String;Landroid/os/IBinder;)V");
+                                 "media/AudioFormat;IIILjava/lang/String;Landroid/os/IBinder;I)V");
     }
     gAudioMixFields.mRule = GetFieldIDOrDie(env, audioMixClass, "mRule",
                                                 "Landroid/media/audiopolicy/AudioMixingRule;");
@@ -3689,6 +3691,7 @@
     gAudioMixFields.mMixType = GetFieldIDOrDie(env, audioMixClass, "mMixType", "I");
     gAudioMixFields.mCallbackFlags = GetFieldIDOrDie(env, audioMixClass, "mCallbackFlags", "I");
     gAudioMixFields.mToken = GetFieldIDOrDie(env, audioMixClass, "mToken", "Landroid/os/IBinder;");
+    gAudioMixFields.mVirtualDeviceId = GetFieldIDOrDie(env, audioMixClass, "mVirtualDeviceId", "I");
 
     jclass audioFormatClass = FindClassOrDie(env, "android/media/AudioFormat");
     gAudioFormatClass = MakeGlobalRefOrDie(env, audioFormatClass);
diff --git a/core/jni/android_os_Trace.cpp b/core/jni/android_os_Trace.cpp
index b579daf..4387a4c 100644
--- a/core/jni/android_os_Trace.cpp
+++ b/core/jni/android_os_Trace.cpp
@@ -124,8 +124,8 @@
     });
 }
 
-static jlong android_os_Trace_nativeGetEnabledTags(JNIEnv* env) {
-    return tracing_perfetto::getEnabledCategories();
+static jboolean android_os_Trace_nativeIsTagEnabled(jlong tag) {
+    return tracing_perfetto::isTagEnabled(tag);
 }
 
 static void android_os_Trace_nativeRegisterWithPerfetto(JNIEnv* env) {
@@ -157,7 +157,7 @@
         {"nativeRegisterWithPerfetto", "()V", (void*)android_os_Trace_nativeRegisterWithPerfetto},
 
         // ----------- @CriticalNative  ----------------
-        {"nativeGetEnabledTags", "()J", (void*)android_os_Trace_nativeGetEnabledTags},
+        {"nativeIsTagEnabled", "(J)Z", (void*)android_os_Trace_nativeIsTagEnabled},
 };
 
 int register_android_os_Trace(JNIEnv* env) {
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 5639a58..a3dba48 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -3249,6 +3249,12 @@
     <!-- Title for EditText context menu [CHAR LIMIT=20] -->
     <string name="editTextMenuTitle">Text actions</string>
 
+    <!-- Error shown when a user uses a stylus to try handwriting on a text field which doesn't support stylus handwriting. [CHAR LIMIT=TOAST] -->
+    <string name="error_handwriting_unsupported">Handwriting is not supported in this field</string>
+
+    <!-- Error shown when a user uses a stylus to try handwriting on a password text field which doesn't support stylus handwriting. [CHAR LIMIT=TOAST] -->
+    <string name="error_handwriting_unsupported_password">Handwriting is not supported in password fields</string>
+
     <!-- Content description of the back button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
     <string name="input_method_nav_back_button_desc">Back</string>
     <!-- Content description of the switch input method button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 668a88c..c2fa297 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3122,6 +3122,8 @@
   <!-- TextView -->
   <java-symbol type="bool" name="config_textShareSupported" />
   <java-symbol type="string" name="failed_to_copy_to_clipboard" />
+  <java-symbol type="string" name="error_handwriting_unsupported" />
+  <java-symbol type="string" name="error_handwriting_unsupported_password" />
 
   <java-symbol type="id" name="notification_material_reply_container" />
   <java-symbol type="id" name="notification_material_reply_text_1" />
diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
index a5c9624..6c00fd8 100644
--- a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
+++ b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
@@ -55,6 +55,7 @@
 import android.view.inputmethod.InputMethodManager;
 import android.widget.EditText;
 
+import androidx.test.annotation.UiThreadTest;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
@@ -72,6 +73,7 @@
  */
 @Presubmit
 @SmallTest
+@UiThreadTest
 @RunWith(AndroidJUnit4.class)
 public class HandwritingInitiatorTest {
     private static final long TIMEOUT = ViewConfiguration.getLongPressTimeout();
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 9c1c7006..ea3235b 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -588,6 +588,8 @@
         <permission name="android.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW" />
         <!-- Permission required for CTS test - PackageManagerShellCommandInstallTest -->
         <permission name="android.permission.EMERGENCY_INSTALL_PACKAGES" />
+        <!-- Permission required for Cts test - CtsSettingsTestCases -->
+        <permission name="android.permission.PREPARE_FACTORY_RESET" />
     </privapp-permissions>
 
     <privapp-permissions package="com.android.statementservice">
diff --git a/data/keyboards/Vendor_054c_Product_05c4.idc b/data/keyboards/Vendor_054c_Product_05c4.idc
index 9576e8d..2da6227 100644
--- a/data/keyboards/Vendor_054c_Product_05c4.idc
+++ b/data/keyboards/Vendor_054c_Product_05c4.idc
@@ -45,14 +45,15 @@
 # This uneven timing causes the apparent speed of a finger (calculated using
 # time deltas between received reports) to vary dramatically even if it's
 # actually moving smoothly across the touchpad, triggering the touchpad stack's
-# drumroll detection logic, which causes the finger's single smooth movement to
-# be treated as many small movements of consecutive touches, which are then
-# inhibited by the click wiggle filter.
+# drumroll detection logic. For moving fingers, the drumroll detection logic
+# splits the finger's single movement into many small movements of consecutive
+# touches, which are then inhibited by the click wiggle filter. For tapping
+# fingers, it prevents tapping to click because it thinks the finger's moving
+# too fast.
 #
-# Since this touchpad does not seem vulnerable to click wiggle, we can safely
-# disable drumroll detection due to speed changes (by setting the speed change
-# threshold very high, since there's no boolean control property).
-gestureProp.Drumroll_Max_Speed_Change_Factor = 1000000000
+# Since this touchpad doesn't seem to have to drumroll issues, we can safely
+# disable drumroll detection.
+gestureProp.Drumroll_Suppression_Enable = 0
 
 # Because of the way this touchpad is positioned, touches around the edges are
 # no more likely to be palms than ones in the middle, so remove the edge zones
diff --git a/data/keyboards/Vendor_054c_Product_09cc.idc b/data/keyboards/Vendor_054c_Product_09cc.idc
index 9576e8d..2a1a4fc 100644
--- a/data/keyboards/Vendor_054c_Product_09cc.idc
+++ b/data/keyboards/Vendor_054c_Product_09cc.idc
@@ -45,14 +45,15 @@
 # This uneven timing causes the apparent speed of a finger (calculated using
 # time deltas between received reports) to vary dramatically even if it's
 # actually moving smoothly across the touchpad, triggering the touchpad stack's
-# drumroll detection logic, which causes the finger's single smooth movement to
-# be treated as many small movements of consecutive touches, which are then
-# inhibited by the click wiggle filter.
+# drumroll detection logic. For moving fingers, the drumroll detection logic
+# splits the finger's single movement into many small movements of consecutive
+# touches, which are then inhibited by the click wiggle filter. For tapping
+# fingers, it prevents tapping to click because it thinks the finger's moving
+# too fast.
 #
-# Since this touchpad does not seem vulnerable to click wiggle, we can safely
-# disable drumroll detection due to speed changes (by setting the speed change
-# threshold very high, since there's no boolean control property).
-gestureProp.Drumroll_Max_Speed_Change_Factor = 1000000000
+# Since this touchpad doesn't seem to have drumroll issues, we can safely
+# disable drumroll detection.
+gestureProp.Drumroll_Suppression_Enable = 0
 
 # Because of the way this touchpad is positioned, touches around the edges are
 # no more likely to be palms than ones in the middle, so remove the edge zones
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
index 1ccc7d8..5f25d70 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
@@ -24,6 +24,7 @@
 import android.tools.flicker.legacy.LegacyFlickerTestFactory
 import android.tools.helpers.WindowUtils
 import android.tools.traces.parsers.toFlickerComponent
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import com.android.server.wm.flicker.testapp.ActivityOptions
@@ -181,6 +182,12 @@
         }
     }
 
+    /** {@inheritDoc} */
+    @FlakyTest(bugId = 312446524)
+    @Test
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+        super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 6f7024a..1fe3c2e 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -5453,7 +5453,8 @@
             String regId = service.registerAudioPolicy(policy.getConfig(), policy.cb(),
                     policy.hasFocusListener(), policy.isFocusPolicy(), policy.isTestFocusPolicy(),
                     policy.isVolumeController(),
-                    projection == null ? null : projection.getProjection());
+                    projection == null ? null : projection.getProjection(),
+                    policy.getAttributionSource());
             if (regId == null) {
                 return ERROR;
             } else {
diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java
index 447d3bb..80e5719 100644
--- a/media/java/android/media/AudioRecord.java
+++ b/media/java/android/media/AudioRecord.java
@@ -789,7 +789,7 @@
         private @NonNull AudioRecord buildAudioPlaybackCaptureRecord() {
             AudioMix audioMix = mAudioPlaybackCaptureConfiguration.createAudioMix(mFormat);
             MediaProjection projection = mAudioPlaybackCaptureConfiguration.getMediaProjection();
-            AudioPolicy audioPolicy = new AudioPolicy.Builder(/*context=*/ null)
+            AudioPolicy audioPolicy = new AudioPolicy.Builder(/*context=*/ mContext)
                     .setMediaProjection(projection)
                     .addMix(audioMix).build();
 
@@ -853,7 +853,7 @@
                     .setFormat(mFormat)
                     .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK)
                     .build();
-            AudioPolicy audioPolicy = new AudioPolicy.Builder(null).addMix(audioMix).build();
+            AudioPolicy audioPolicy = new AudioPolicy.Builder(mContext).addMix(audioMix).build();
             if (AudioManager.registerAudioPolicyStatic(audioPolicy) != 0) {
                 throw new UnsupportedOperationException("Error: could not register audio policy");
             }
diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java
index 194da21..73deb17 100644
--- a/media/java/android/media/AudioTrack.java
+++ b/media/java/android/media/AudioTrack.java
@@ -1353,7 +1353,8 @@
                     .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK)
                     .build();
             AudioPolicy audioPolicy =
-                    new AudioPolicy.Builder(/*context=*/ null).addMix(audioMix).build();
+                    new AudioPolicy.Builder(/*context=*/ mContext).addMix(audioMix).build();
+
             if (AudioManager.registerAudioPolicyStatic(audioPolicy) != 0) {
                 throw new UnsupportedOperationException("Error: could not register audio policy");
             }
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 98bd3ca..e612645 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -18,6 +18,7 @@
 
 import android.bluetooth.BluetoothDevice;
 import android.content.ComponentName;
+import android.content.AttributionSource;
 import android.media.AudioAttributes;
 import android.media.AudioDeviceAttributes;
 import android.media.AudioFormat;
@@ -361,7 +362,8 @@
     String registerAudioPolicy(in AudioPolicyConfig policyConfig,
             in IAudioPolicyCallback pcb, boolean hasFocusListener, boolean isFocusPolicy,
             boolean isTestFocusPolicy,
-            boolean isVolumeController, in IMediaProjection projection);
+            boolean isVolumeController, in IMediaProjection projection,
+            in AttributionSource attributionSource);
 
     oneway void unregisterAudioPolicyAsync(in IAudioPolicyCallback pcb);
 
diff --git a/media/java/android/media/MediaCas.java b/media/java/android/media/MediaCas.java
index ab7c27f..2d7db5e 100644
--- a/media/java/android/media/MediaCas.java
+++ b/media/java/android/media/MediaCas.java
@@ -35,6 +35,7 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.os.IBinder;
 import android.os.IHwBinder;
 import android.os.Looper;
 import android.os.Message;
@@ -43,7 +44,6 @@
 import android.os.ServiceManager;
 import android.os.ServiceSpecificException;
 import android.util.Log;
-import android.util.Singleton;
 
 import com.android.internal.util.FrameworkStatsLog;
 
@@ -264,71 +264,107 @@
     public static final int PLUGIN_STATUS_SESSION_NUMBER_CHANGED =
             android.hardware.cas.StatusEvent.PLUGIN_SESSION_NUMBER_CHANGED;
 
-    private static final Singleton<IMediaCasService> sService =
-            new Singleton<IMediaCasService>() {
+    private static IMediaCasService sService = null;
+    private static Object sAidlLock = new Object();
+
+    /** DeathListener for AIDL service */
+    private static IBinder.DeathRecipient sDeathListener =
+            new IBinder.DeathRecipient() {
                 @Override
-                protected IMediaCasService create() {
-                    try {
-                        Log.d(TAG, "Trying to get AIDL service");
-                        IMediaCasService serviceAidl =
-                                IMediaCasService.Stub.asInterface(
-                                        ServiceManager.waitForDeclaredService(
-                                                IMediaCasService.DESCRIPTOR + "/default"));
-                        if (serviceAidl != null) {
-                            return serviceAidl;
-                        }
-                    } catch (Exception eAidl) {
-                        Log.d(TAG, "Failed to get cas AIDL service");
+                public void binderDied() {
+                    synchronized (sAidlLock) {
+                        Log.d(TAG, "The service is dead");
+                        sService.asBinder().unlinkToDeath(sDeathListener, 0);
+                        sService = null;
                     }
-                    return null;
-                }
-            };
-
-    private static final Singleton<android.hardware.cas.V1_0.IMediaCasService> sServiceHidl =
-            new Singleton<android.hardware.cas.V1_0.IMediaCasService>() {
-                @Override
-                protected android.hardware.cas.V1_0.IMediaCasService create() {
-                    try {
-                        Log.d(TAG, "Trying to get cas@1.2 service");
-                        android.hardware.cas.V1_2.IMediaCasService serviceV12 =
-                                android.hardware.cas.V1_2.IMediaCasService.getService(
-                                        true /*wait*/);
-                        if (serviceV12 != null) {
-                            return serviceV12;
-                        }
-                    } catch (Exception eV1_2) {
-                        Log.d(TAG, "Failed to get cas@1.2 service");
-                    }
-
-                    try {
-                        Log.d(TAG, "Trying to get cas@1.1 service");
-                        android.hardware.cas.V1_1.IMediaCasService serviceV11 =
-                                android.hardware.cas.V1_1.IMediaCasService.getService(
-                                        true /*wait*/);
-                        if (serviceV11 != null) {
-                            return serviceV11;
-                        }
-                    } catch (Exception eV1_1) {
-                        Log.d(TAG, "Failed to get cas@1.1 service");
-                    }
-
-                    try {
-                        Log.d(TAG, "Trying to get cas@1.0 service");
-                        return android.hardware.cas.V1_0.IMediaCasService.getService(true /*wait*/);
-                    } catch (Exception eV1_0) {
-                        Log.d(TAG, "Failed to get cas@1.0 service");
-                    }
-
-                    return null;
                 }
             };
 
     static IMediaCasService getService() {
-        return sService.get();
+        synchronized (sAidlLock) {
+            if (sService == null || !sService.asBinder().isBinderAlive()) {
+                try {
+                    Log.d(TAG, "Trying to get AIDL service");
+                    sService =
+                            IMediaCasService.Stub.asInterface(
+                                    ServiceManager.waitForDeclaredService(
+                                            IMediaCasService.DESCRIPTOR + "/default"));
+                    if (sService != null) {
+                        sService.asBinder().linkToDeath(sDeathListener, 0);
+                    }
+                } catch (Exception eAidl) {
+                    Log.d(TAG, "Failed to get cas AIDL service");
+                }
+            }
+            return sService;
+        }
     }
 
+    private static android.hardware.cas.V1_0.IMediaCasService sServiceHidl = null;
+    private static Object sHidlLock = new Object();
+
+    /** Used to indicate the right end-point to handle the serviceDied method */
+    private static final long MEDIA_CAS_HIDL_COOKIE = 394;
+
+    /** DeathListener for HIDL service */
+    private static IHwBinder.DeathRecipient sDeathListenerHidl =
+            new IHwBinder.DeathRecipient() {
+                @Override
+                public void serviceDied(long cookie) {
+                    if (cookie == MEDIA_CAS_HIDL_COOKIE) {
+                        synchronized (sHidlLock) {
+                            sServiceHidl = null;
+                        }
+                    }
+                }
+            };
+
     static android.hardware.cas.V1_0.IMediaCasService getServiceHidl() {
-        return sServiceHidl.get();
+        synchronized (sHidlLock) {
+            if (sServiceHidl != null) {
+                return sServiceHidl;
+            } else {
+                try {
+                    Log.d(TAG, "Trying to get cas@1.2 service");
+                    android.hardware.cas.V1_2.IMediaCasService serviceV12 =
+                            android.hardware.cas.V1_2.IMediaCasService.getService(true /*wait*/);
+                    if (serviceV12 != null) {
+                        sServiceHidl = serviceV12;
+                        sServiceHidl.linkToDeath(sDeathListenerHidl, MEDIA_CAS_HIDL_COOKIE);
+                        return sServiceHidl;
+                    }
+                } catch (Exception eV1_2) {
+                    Log.d(TAG, "Failed to get cas@1.2 service");
+                }
+
+                try {
+                    Log.d(TAG, "Trying to get cas@1.1 service");
+                    android.hardware.cas.V1_1.IMediaCasService serviceV11 =
+                            android.hardware.cas.V1_1.IMediaCasService.getService(true /*wait*/);
+                    if (serviceV11 != null) {
+                        sServiceHidl = serviceV11;
+                        sServiceHidl.linkToDeath(sDeathListenerHidl, MEDIA_CAS_HIDL_COOKIE);
+                        return sServiceHidl;
+                    }
+                } catch (Exception eV1_1) {
+                    Log.d(TAG, "Failed to get cas@1.1 service");
+                }
+
+                try {
+                    Log.d(TAG, "Trying to get cas@1.0 service");
+                    sServiceHidl =
+                            android.hardware.cas.V1_0.IMediaCasService.getService(true /*wait*/);
+                    if (sServiceHidl != null) {
+                        sServiceHidl.linkToDeath(sDeathListenerHidl, MEDIA_CAS_HIDL_COOKIE);
+                    }
+                    return sServiceHidl;
+                } catch (Exception eV1_0) {
+                    Log.d(TAG, "Failed to get cas@1.0 service");
+                }
+            }
+        }
+        // Couldn't find an HIDL service, returning null.
+        return null;
     }
 
     private void validateInternalStates() {
@@ -756,7 +792,7 @@
      * @return Whether the specified CA system is supported on this device.
      */
     public static boolean isSystemIdSupported(int CA_system_id) {
-        IMediaCasService service = sService.get();
+        IMediaCasService service = getService();
         if (service != null) {
             try {
                 return service.isSystemIdSupported(CA_system_id);
@@ -765,7 +801,7 @@
             }
         }
 
-        android.hardware.cas.V1_0.IMediaCasService serviceHidl = sServiceHidl.get();
+        android.hardware.cas.V1_0.IMediaCasService serviceHidl = getServiceHidl();
         if (serviceHidl != null) {
             try {
                 return serviceHidl.isSystemIdSupported(CA_system_id);
@@ -781,7 +817,7 @@
      * @return an array of descriptors for the available CA plugins.
      */
     public static PluginDescriptor[] enumeratePlugins() {
-        IMediaCasService service = sService.get();
+        IMediaCasService service = getService();
         if (service != null) {
             try {
                 AidlCasPluginDescriptor[] descriptors = service.enumeratePlugins();
@@ -794,10 +830,11 @@
                 }
                 return results;
             } catch (RemoteException e) {
+                Log.e(TAG, "Some exception while enumerating plugins");
             }
         }
 
-        android.hardware.cas.V1_0.IMediaCasService serviceHidl = sServiceHidl.get();
+        android.hardware.cas.V1_0.IMediaCasService serviceHidl = getServiceHidl();
         if (serviceHidl != null) {
             try {
                 ArrayList<HidlCasPluginDescriptor> descriptors = serviceHidl.enumeratePlugins();
diff --git a/media/java/android/media/audiopolicy/AudioMix.java b/media/java/android/media/audiopolicy/AudioMix.java
index a53a8ce..e4eaaa3 100644
--- a/media/java/android/media/audiopolicy/AudioMix.java
+++ b/media/java/android/media/audiopolicy/AudioMix.java
@@ -24,6 +24,7 @@
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
 import android.media.AudioDeviceInfo;
 import android.media.AudioFormat;
 import android.media.AudioSystem;
@@ -67,12 +68,19 @@
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     final int mDeviceSystemType; // an AudioSystem.DEVICE_* value, not AudioDeviceInfo.TYPE_*
 
+    // The (virtual) device ID that this AudioMix was registered for. This value is overwritten
+    // when registering this AudioMix with an AudioPolicy or attaching this AudioMix to an
+    // AudioPolicy to match the AudioPolicy attribution. Does not imply that it only modifies
+    // audio routing for this device ID.
+    private int mVirtualDeviceId;
+
     /**
      * All parameters are guaranteed valid through the Builder.
      */
     private AudioMix(@NonNull AudioMixingRule rule, @NonNull AudioFormat format,
             int routeFlags, int callbackFlags,
-            int deviceType, @Nullable String deviceAddress, IBinder token) {
+            int deviceType, @Nullable String deviceAddress, IBinder token,
+            int virtualDeviceId) {
         mRule = Objects.requireNonNull(rule);
         mFormat = Objects.requireNonNull(format);
         mRouteFlags = routeFlags;
@@ -81,6 +89,7 @@
         mDeviceSystemType = deviceType;
         mDeviceAddress = (deviceAddress == null) ? new String("") : deviceAddress;
         mToken = token;
+        mVirtualDeviceId = virtualDeviceId;
     }
 
     // CALLBACK_FLAG_* values: keep in sync with AudioMix::kCbFlag* values defined
@@ -269,6 +278,11 @@
     }
 
     /** @hide */
+    public boolean matchesVirtualDeviceId(int deviceId) {
+        return mVirtualDeviceId == deviceId;
+    }
+
+    /** @hide */
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
@@ -311,6 +325,7 @@
         mFormat.writeToParcel(dest, flags);
         mRule.writeToParcel(dest, flags);
         dest.writeStrongBinder(mToken);
+        dest.writeInt(mVirtualDeviceId);
     }
 
     public static final @NonNull Parcelable.Creator<AudioMix> CREATOR = new Parcelable.Creator<>() {
@@ -331,6 +346,7 @@
             mixBuilder.setFormat(AudioFormat.CREATOR.createFromParcel(p));
             mixBuilder.setMixingRule(AudioMixingRule.CREATOR.createFromParcel(p));
             mixBuilder.setToken(p.readStrongBinder());
+            mixBuilder.setVirtualDeviceId(p.readInt());
             return mixBuilder.build();
         }
 
@@ -339,6 +355,15 @@
         }
     };
 
+    /**
+     * Updates the deviceId of the AudioMix to match with the AudioPolicy the mix is registered
+     * through.
+     * @hide
+     */
+    public void setVirtualDeviceId(int virtualDeviceId) {
+        mVirtualDeviceId = virtualDeviceId;
+    }
+
     /** @hide */
     @IntDef(flag = true,
             value = { ROUTE_FLAG_RENDER, ROUTE_FLAG_LOOP_BACK } )
@@ -354,6 +379,7 @@
         private int mRouteFlags = 0;
         private int mCallbackFlags = 0;
         private IBinder mToken = null;
+        private int mVirtualDeviceId = Context.DEVICE_ID_DEFAULT;
         // an AudioSystem.DEVICE_* value, not AudioDeviceInfo.TYPE_*
         private int mDeviceSystemType = AudioSystem.DEVICE_NONE;
         private String mDeviceAddress = null;
@@ -404,6 +430,15 @@
 
         /**
          * @hide
+         * Only used by AudioMix internally.
+         */
+        Builder setVirtualDeviceId(int virtualDeviceId) {
+            mVirtualDeviceId = virtualDeviceId;
+            return this;
+        }
+
+        /**
+         * @hide
          * Only used by AudioPolicyConfig, not a public API.
          * @param callbackFlags which callbacks are called from native
          * @return the same Builder instance.
@@ -570,7 +605,7 @@
             }
 
             return new AudioMix(mRule, mFormat, mRouteFlags, mCallbackFlags, mDeviceSystemType,
-                    mDeviceAddress, mToken);
+                    mDeviceAddress, mToken, mVirtualDeviceId);
         }
 
         private int getLoopbackDeviceSystemTypeForAudioMixingRule(AudioMixingRule rule) {
diff --git a/media/java/android/media/audiopolicy/AudioPolicy.java b/media/java/android/media/audiopolicy/AudioPolicy.java
index 508c0a2b..293a8f8 100644
--- a/media/java/android/media/audiopolicy/AudioPolicy.java
+++ b/media/java/android/media/audiopolicy/AudioPolicy.java
@@ -27,6 +27,7 @@
 import android.annotation.TestApi;
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
+import android.content.AttributionSource;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.media.AudioAttributes;
@@ -146,6 +147,16 @@
         return mProjection;
     }
 
+    /** @hide */
+    public AttributionSource getAttributionSource() {
+        return getAttributionSource(mContext);
+    }
+
+    private static AttributionSource getAttributionSource(Context context) {
+        return context == null
+                ? AttributionSource.myAttributionSource() : context.getAttributionSource();
+    }
+
     /**
      * The parameters are guaranteed non-null through the Builder
      */
@@ -208,6 +219,9 @@
             if (mix == null) {
                 throw new IllegalArgumentException("Illegal null AudioMix argument");
             }
+            if (android.permission.flags.Flags.deviceAwarePermissionApisEnabled()) {
+                mix.setVirtualDeviceId(getAttributionSource(mContext).getDeviceId());
+            }
             mMixes.add(mix);
             return this;
         }
@@ -358,6 +372,9 @@
                 if (mix == null) {
                     throw new IllegalArgumentException("Illegal null AudioMix in attachMixes");
                 } else {
+                    if (android.permission.flags.Flags.deviceAwarePermissionApisEnabled()) {
+                        mix.setVirtualDeviceId(getAttributionSource(mContext).getDeviceId());
+                    }
                     zeMixes.add(mix);
                 }
             }
@@ -400,6 +417,9 @@
                 if (mix == null) {
                     throw new IllegalArgumentException("Illegal null AudioMix in detachMixes");
                 } else {
+                    if (android.permission.flags.Flags.deviceAwarePermissionApisEnabled()) {
+                        mix.setVirtualDeviceId(getAttributionSource(mContext).getDeviceId());
+                    }
                     zeMixes.add(mix);
                 }
             }
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
index 32795e4..e48c0f4 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
@@ -96,6 +96,7 @@
     var stagedSessionId = SessionInfo.INVALID_ID
         private set
     private var callingUid = Process.INVALID_UID
+    private var originatingUid = Process.INVALID_UID
     private var callingPackage: String? = null
     private var sessionStager: SessionStager? = null
     private lateinit var intent: Intent
@@ -148,7 +149,7 @@
         }
         val sourceInfo: ApplicationInfo? = getSourceInfo(callingPackage)
         // Uid of the source package, with a preference to uid from ApplicationInfo
-        val originatingUid = sourceInfo?.uid ?: callingUid
+        originatingUid = sourceInfo?.uid ?: callingUid
         appOpRequestInfo = AppOpRequestInfo(
             getPackageNameForUid(context, originatingUid, callingPackage),
             originatingUid, callingAttributionTag
@@ -282,7 +283,7 @@
                     context.contentResolver.openAssetFileDescriptor(uri, "r").use { afd ->
                         val pfd: ParcelFileDescriptor? = afd?.parcelFileDescriptor
                         val params: SessionParams =
-                            createSessionParams(intent, pfd, uri.toString())
+                            createSessionParams(originatingUid, intent, pfd, uri.toString())
                         stagedSessionId = packageInstaller.createSession(params)
                     }
                 } catch (e: Exception) {
@@ -338,6 +339,7 @@
     }
 
     private fun createSessionParams(
+        originatingUid: Int,
         intent: Intent,
         pfd: ParcelFileDescriptor?,
         debugPathName: String,
@@ -354,9 +356,7 @@
         params.setOriginatingUri(
             intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI, Uri::class.java)
         )
-        params.setOriginatingUid(
-            intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID, Process.INVALID_UID)
-        )
+        params.setOriginatingUid(originatingUid)
         params.setInstallerPackageName(intent.getStringExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME))
         params.setInstallReason(PackageManager.INSTALL_REASON_USER)
         // Disable full screen intent usage by for sideloads.
diff --git a/packages/SettingsLib/ProfileSelector/Android.bp b/packages/SettingsLib/ProfileSelector/Android.bp
index 6dc07b2..4aa67c1 100644
--- a/packages/SettingsLib/ProfileSelector/Android.bp
+++ b/packages/SettingsLib/ProfileSelector/Android.bp
@@ -20,6 +20,7 @@
     static_libs: [
         "com.google.android.material_material",
         "SettingsLibSettingsTheme",
+        "android.os.flags-aconfig-java-export",
     ],
 
     sdk_version: "system_current",
diff --git a/packages/SettingsLib/ProfileSelector/AndroidManifest.xml b/packages/SettingsLib/ProfileSelector/AndroidManifest.xml
index 80f6b76..303e20c 100644
--- a/packages/SettingsLib/ProfileSelector/AndroidManifest.xml
+++ b/packages/SettingsLib/ProfileSelector/AndroidManifest.xml
@@ -18,5 +18,5 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.android.settingslib.widget.profileselector">
 
-    <uses-sdk android:minSdkVersion="23" />
+    <uses-sdk android:minSdkVersion="29" />
 </manifest>
diff --git a/packages/SettingsLib/ProfileSelector/res/values/strings.xml b/packages/SettingsLib/ProfileSelector/res/values/strings.xml
index 68d4047..76ccb65 100644
--- a/packages/SettingsLib/ProfileSelector/res/values/strings.xml
+++ b/packages/SettingsLib/ProfileSelector/res/values/strings.xml
@@ -21,4 +21,6 @@
     <string name="settingslib_category_personal">Personal</string>
     <!-- Header for items under the work user [CHAR LIMIT=30] -->
     <string name="settingslib_category_work">Work</string>
+    <!-- Header for items under the private profile user [CHAR LIMIT=30] -->
+    <string name="settingslib_category_private">Private</string>
 </resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java
index be5753be..c52386b 100644
--- a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java
+++ b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java
@@ -16,31 +16,77 @@
 
 package com.android.settingslib.widget;
 
+import android.annotation.TargetApi;
 import android.app.Activity;
+import android.content.Context;
+import android.content.pm.UserProperties;
+import android.os.Build;
 import android.os.Bundle;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.ArrayMap;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 
+import androidx.core.os.BuildCompat;
 import androidx.fragment.app.Fragment;
 import androidx.viewpager2.widget.ViewPager2;
 
+import com.android.settingslib.widget.profileselector.R;
+
 import com.google.android.material.tabs.TabLayout;
 import com.google.android.material.tabs.TabLayoutMediator;
-import com.android.settingslib.widget.profileselector.R;
+
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Base fragment class for profile settings.
  */
 public abstract class ProfileSelectFragment extends Fragment {
+    private static final String TAG = "ProfileSelectFragment";
+    // UserHandle#USER_NULL is a @TestApi so is not accessible.
+    private static final int USER_NULL = -10000;
+    private static final int DEFAULT_POSITION = 0;
 
     /**
-     * Personal or Work profile tab of {@link ProfileSelectFragment}
-     * <p>0: Personal tab.
-     * <p>1: Work profile tab.
+     * The type of profile tab of {@link ProfileSelectFragment} to show
+     * <ul>
+     *   <li>0: Personal tab.
+     *   <li>1: Work profile tab.
+     * </ul>
+     *
+     * <p> Please note that this is supported for legacy reasons. Please use
+     * {@link #EXTRA_SHOW_FRAGMENT_USER_ID} instead.
      */
-    public static final String EXTRA_SHOW_FRAGMENT_TAB =
-            ":settings:show_fragment_tab";
+    public static final String EXTRA_SHOW_FRAGMENT_TAB = ":settings:show_fragment_tab";
+
+    /**
+     * An {@link ArrayList} of users to show. The supported users are: System user, the managed
+     * profile user, and the private profile user. A client should pass all the user ids that need
+     * to be shown in this list. Note that if this list is not provided then, for legacy reasons
+     * see {@link #EXTRA_SHOW_FRAGMENT_TAB}, an attempt will be made to show two tabs: one for the
+     * System user and one for the managed profile user.
+     *
+     * <p>Please note that this MUST be used in conjunction with
+     * {@link #EXTRA_SHOW_FRAGMENT_USER_ID}
+     */
+    public static final String EXTRA_LIST_OF_USER_IDS = ":settings:list_user_ids";
+
+    /**
+     * The user id of the user to be show in {@link ProfileSelectFragment}. Only the below user
+     * types are supported:
+     * <ul>
+     *   <li> System user.
+     *   <li> Managed profile user.
+     *   <li> Private profile user.
+     * </ul>
+     *
+     * <p>Please note that this MUST be used in conjunction with {@link #EXTRA_LIST_OF_USER_IDS}.
+     */
+    public static final String EXTRA_SHOW_FRAGMENT_USER_ID = ":settings:show_fragment_user_id";
 
     /**
      * Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB
@@ -48,13 +94,23 @@
     public static final int PERSONAL_TAB = 0;
 
     /**
-     * Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB
+     * Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB for the managed profile
      */
     public static final int WORK_TAB = 1;
 
+    /**
+     * Please note that private profile is available from API LEVEL
+     * {@link Build.VERSION_CODES.VANILLA_ICE_CREAM} only, therefore PRIVATE_TAB MUST be
+     * passed in {@link #EXTRA_SHOW_FRAGMENT_TAB} and {@link #EXTRA_LIST_OF_PROFILE_TABS} for
+     * {@link Build.VERSION_CODES.VANILLA_ICE_CREAM} or higher API Levels only.
+     */
+    private static final int PRIVATE_TAB = 2;
+
     private ViewGroup mContentView;
 
     private ViewPager2 mViewPager;
+    private final ArrayMap<UserHandle, Integer> mProfileTabsByUsers = new ArrayMap<>();
+    private boolean mUsingUserIds = false;
 
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
@@ -67,7 +123,7 @@
         if (titleResId > 0) {
             activity.setTitle(titleResId);
         }
-        final int selectedTab = getTabId(activity, getArguments());
+        initProfileTabsToShow();
 
         final View tabContainer = mContentView.findViewById(R.id.tab_container);
         mViewPager = tabContainer.findViewById(R.id.view_pager);
@@ -78,16 +134,14 @@
         ).attach();
 
         tabContainer.setVisibility(View.VISIBLE);
-        final TabLayout.Tab tab = tabs.getTabAt(selectedTab);
+        final TabLayout.Tab tab = tabs.getTabAt(getSelectedTabPosition(activity, getArguments()));
         tab.select();
 
         return mContentView;
     }
 
     /**
-     * create Personal or Work profile fragment
-     * <p>0: Personal profile.
-     * <p>1: Work profile.
+     * Create Personal or Work or Private profile fragment. See {@link #EXTRA_SHOW_FRAGMENT_USER_ID}
      */
     public abstract Fragment createFragment(int position);
 
@@ -99,21 +153,90 @@
         return 0;
     }
 
-    int getTabId(Activity activity, Bundle bundle) {
+    int getSelectedTabPosition(Activity activity, Bundle bundle) {
         if (bundle != null) {
+            final int extraUserId = bundle.getInt(EXTRA_SHOW_FRAGMENT_USER_ID, USER_NULL);
+            if (extraUserId != USER_NULL) {
+                return mProfileTabsByUsers.indexOfKey(UserHandle.of(extraUserId));
+            }
             final int extraTab = bundle.getInt(EXTRA_SHOW_FRAGMENT_TAB, -1);
             if (extraTab != -1) {
                 return extraTab;
             }
         }
-        return PERSONAL_TAB;
+        return DEFAULT_POSITION;
+    }
+
+    int getTabCount() {
+        return mUsingUserIds ? mProfileTabsByUsers.size() : 2;
+    }
+
+    void initProfileTabsToShow() {
+        Bundle bundle = getArguments();
+        if (bundle != null) {
+            ArrayList<Integer> userIdsToShow =
+                    bundle.getIntegerArrayList(EXTRA_LIST_OF_USER_IDS);
+            if (userIdsToShow != null && !userIdsToShow.isEmpty()) {
+                mUsingUserIds = true;
+                UserManager userManager = getContext().getSystemService(UserManager.class);
+                List<UserHandle> userHandles = userManager.getUserProfiles();
+                for (UserHandle userHandle : userHandles) {
+                    if (!userIdsToShow.contains(userHandle.getIdentifier())) {
+                        continue;
+                    }
+                    if (userHandle.isSystem()) {
+                        mProfileTabsByUsers.put(userHandle, PERSONAL_TAB);
+                    } else if (userManager.isManagedProfile(userHandle.getIdentifier())) {
+                        mProfileTabsByUsers.put(userHandle, WORK_TAB);
+                    } else if (shouldShowPrivateProfileIfItsOne(userHandle)) {
+                        mProfileTabsByUsers.put(userHandle, PRIVATE_TAB);
+                    }
+                }
+            }
+        }
+    }
+
+    private int getProfileTabForPosition(int position) {
+        return mUsingUserIds ? mProfileTabsByUsers.valueAt(position) : position;
+    }
+
+    int getUserIdForPosition(int position) {
+        return mUsingUserIds ? mProfileTabsByUsers.keyAt(position).getIdentifier() : position;
     }
 
     private CharSequence getPageTitle(int position) {
-        if (position == WORK_TAB) {
+        int tab = getProfileTabForPosition(position);
+        if (tab == WORK_TAB) {
             return getContext().getString(R.string.settingslib_category_work);
+        } else if (tab == PRIVATE_TAB) {
+            return getContext().getString(R.string.settingslib_category_private);
         }
 
         return getString(R.string.settingslib_category_personal);
     }
+
+    @TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    private boolean shouldShowUserInQuietMode(UserHandle userHandle, UserManager userManager) {
+        UserProperties userProperties = userManager.getUserProperties(userHandle);
+        return !userManager.isQuietModeEnabled(userHandle)
+                || userProperties.getShowInQuietMode() != UserProperties.SHOW_IN_QUIET_MODE_HIDDEN;
+    }
+
+    // It's sufficient to have this method marked with the appropriate API level because we expect
+    // to be here only for this API level - when then private profile was introduced.
+    @TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    private boolean shouldShowPrivateProfileIfItsOne(UserHandle userHandle) {
+        if (!BuildCompat.isAtLeastV() || !android.os.Flags.allowPrivateProfile()) {
+            return false;
+        }
+        try {
+            Context userContext = getContext().createContextAsUser(userHandle, /* flags= */ 0);
+            UserManager userManager = userContext.getSystemService(UserManager.class);
+            return userManager.isPrivateProfile()
+                    && shouldShowUserInQuietMode(userHandle, userManager);
+        } catch (IllegalStateException exception) {
+            Log.i(TAG, "Ignoring this user as the calling package not available in this user.");
+        }
+        return false;
+    }
 }
diff --git a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java
index f5ab647..37f4f27 100644
--- a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java
+++ b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java
@@ -18,7 +18,6 @@
 
 import androidx.fragment.app.Fragment;
 import androidx.viewpager2.adapter.FragmentStateAdapter;
-import com.android.settingslib.widget.profileselector.R;
 
 /**
  * ViewPager Adapter to handle between TabLayout and ViewPager2
@@ -34,11 +33,11 @@
 
     @Override
     public Fragment createFragment(int position) {
-        return mParentFragments.createFragment(position);
+        return mParentFragments.createFragment(mParentFragments.getUserIdForPosition(position));
     }
 
     @Override
     public int getItemCount() {
-        return 2;
+        return mParentFragments.getTabCount();
     }
 }
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 02d19dc..5804071 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -932,6 +932,9 @@
     <uses-permission
         android:name="android.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW" />
 
+    <!-- Permission required for Cts test - CtsSettingsTestCases -->
+    <uses-permission android:name="android.permission.PREPARE_FACTORY_RESET" />
+
     <application
         android:label="@string/app_label"
         android:theme="@android:style/Theme.DeviceDefault.DayNight"
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
index 6546b87..f70ad9e 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
+++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
@@ -23,10 +23,10 @@
 import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_RECENTS;
 import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT;
 
-import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_OPEN_BLOCKED;
 import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_GLOBAL_ACTION;
 import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_GLOBAL_ACTION_EXTRA;
 import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_HIDE_MENU;
+import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_OPEN_BLOCKED;
 import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_TOGGLE_MENU;
 import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.PACKAGE_NAME;
 
@@ -77,6 +77,8 @@
     private static final int TIMEOUT_SERVICE_STATUS_CHANGE_S = 5;
     private static final int TIMEOUT_UI_CHANGE_S = 5;
     private static final int NO_GLOBAL_ACTION = -1;
+    private static final Intent INTENT_OPEN_MENU = new Intent(INTENT_TOGGLE_MENU)
+            .setPackage(PACKAGE_NAME);
 
     private static Instrumentation sInstrumentation;
     private static UiAutomation sUiAutomation;
@@ -152,9 +154,6 @@
     @Before
     public void setup() throws Throwable {
         sOpenBlocked.set(false);
-        wakeUpScreen();
-        sUiAutomation.executeShellCommand("input keyevent KEYCODE_MENU");
-        openMenu();
     }
 
     @After
@@ -188,24 +187,17 @@
     }
 
     private static void openMenu() throws Throwable {
-        openMenu(false);
-    }
-
-    private static void openMenu(boolean abandonOnBlock) throws Throwable {
-        Intent intent = new Intent(INTENT_TOGGLE_MENU);
-        intent.setPackage(PACKAGE_NAME);
-        sInstrumentation.getContext().sendBroadcast(intent);
+        unlockSignal();
+        sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU);
 
         TestUtils.waitUntil("Timed out before menu could appear.",
                 TIMEOUT_UI_CHANGE_S,
                 () -> {
-                    if (sOpenBlocked.get() && abandonOnBlock) {
-                        throw new IllegalStateException();
-                    }
                     if (isMenuVisible()) {
                         return true;
                     } else {
-                        sInstrumentation.getContext().sendBroadcast(intent);
+                        unlockSignal();
+                        sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU);
                         return false;
                     }
                 });
@@ -249,6 +241,7 @@
 
     @Test
     public void testAdjustBrightness() throws Throwable {
+        openMenu();
         Context context = sInstrumentation.getTargetContext();
         DisplayManager displayManager = context.getSystemService(
                 DisplayManager.class);
@@ -264,22 +257,28 @@
                 context.getDisplayId()).getBrightnessInfo();
 
         try {
-            displayManager.setBrightness(context.getDisplayId(), brightnessInfo.brightnessMinimum);
             TestUtils.waitUntil("Could not change to minimum brightness",
                     TIMEOUT_UI_CHANGE_S,
-                    () -> displayManager.getBrightness(context.getDisplayId())
-                            == brightnessInfo.brightnessMinimum);
+                    () -> {
+                        displayManager.setBrightness(
+                                context.getDisplayId(), brightnessInfo.brightnessMinimum);
+                        return displayManager.getBrightness(context.getDisplayId())
+                                == brightnessInfo.brightnessMinimum;
+                    });
             brightnessUpButton.performAction(CLICK_ID);
             TestUtils.waitUntil("Did not detect an increase in brightness.",
                     TIMEOUT_UI_CHANGE_S,
                     () -> displayManager.getBrightness(context.getDisplayId())
                             > brightnessInfo.brightnessMinimum);
 
-            displayManager.setBrightness(context.getDisplayId(), brightnessInfo.brightnessMaximum);
             TestUtils.waitUntil("Could not change to maximum brightness",
                     TIMEOUT_UI_CHANGE_S,
-                    () -> displayManager.getBrightness(context.getDisplayId())
-                            == brightnessInfo.brightnessMaximum);
+                    () -> {
+                        displayManager.setBrightness(
+                                context.getDisplayId(), brightnessInfo.brightnessMaximum);
+                        return displayManager.getBrightness(context.getDisplayId())
+                                == brightnessInfo.brightnessMaximum;
+                    });
             brightnessDownButton.performAction(CLICK_ID);
             TestUtils.waitUntil("Did not detect a decrease in brightness.",
                     TIMEOUT_UI_CHANGE_S,
@@ -292,6 +291,7 @@
 
     @Test
     public void testAdjustVolume() throws Throwable {
+        openMenu();
         Context context = sInstrumentation.getTargetContext();
         AudioManager audioManager = context.getSystemService(AudioManager.class);
         int resetVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
@@ -332,6 +332,7 @@
 
     @Test
     public void testAssistantButton_opensVoiceAssistant() throws Throwable {
+        openMenu();
         AccessibilityNodeInfo assistantButton = findGridButtonInfo(getGridButtonList(),
                 String.valueOf(ShortcutId.ID_ASSISTANT_VALUE.ordinal()));
         Intent expectedIntent = new Intent(Intent.ACTION_VOICE_COMMAND);
@@ -349,6 +350,7 @@
 
     @Test
     public void testAccessibilitySettingsButton_opensAccessibilitySettings() throws Throwable {
+        openMenu();
         AccessibilityNodeInfo settingsButton = findGridButtonInfo(getGridButtonList(),
                 String.valueOf(ShortcutId.ID_A11YSETTING_VALUE.ordinal()));
         Intent expectedIntent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
@@ -364,6 +366,7 @@
 
     @Test
     public void testPowerButton_performsGlobalAction() throws Throwable {
+        openMenu();
         AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
                 String.valueOf(ShortcutId.ID_POWER_VALUE.ordinal()));
 
@@ -376,6 +379,7 @@
 
     @Test
     public void testRecentButton_performsGlobalAction() throws Throwable {
+        openMenu();
         AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
                 String.valueOf(ShortcutId.ID_RECENT_VALUE.ordinal()));
 
@@ -388,6 +392,7 @@
 
     @Test
     public void testLockButton_performsGlobalAction() throws Throwable {
+        openMenu();
         AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
                 String.valueOf(ShortcutId.ID_LOCKSCREEN_VALUE.ordinal()));
 
@@ -400,6 +405,7 @@
 
     @Test
     public void testQuickSettingsButton_performsGlobalAction() throws Throwable {
+        openMenu();
         AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
                 String.valueOf(ShortcutId.ID_QUICKSETTING_VALUE.ordinal()));
 
@@ -412,6 +418,7 @@
 
     @Test
     public void testNotificationsButton_performsGlobalAction() throws Throwable {
+        openMenu();
         AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
                 String.valueOf(ShortcutId.ID_NOTIFICATION_VALUE.ordinal()));
 
@@ -424,6 +431,7 @@
 
     @Test
     public void testScreenshotButton_performsGlobalAction() throws Throwable {
+        openMenu();
         AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
                 String.valueOf(ShortcutId.ID_SCREENSHOT_VALUE.ordinal()));
 
@@ -436,6 +444,7 @@
 
     @Test
     public void testOnScreenLock_closesMenu() throws Throwable {
+        openMenu();
         closeScreen();
         wakeUpScreen();
 
@@ -447,13 +456,18 @@
         closeScreen();
         wakeUpScreen();
 
-        boolean blocked = false;
-        try {
-            openMenu(true);
-        } catch (IllegalStateException e) {
-            // Expected
-            blocked = true;
-        }
-        assertThat(blocked).isTrue();
+        TestUtils.waitUntil("Did not receive signal that menu cannot open",
+                TIMEOUT_UI_CHANGE_S,
+                () -> {
+                    sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU);
+                    return sOpenBlocked.get();
+                });
+    }
+
+    private static void unlockSignal() {
+        // MENU unlocks screen,
+        // BACK closes any menu that may appear if the screen wasn't locked.
+        sUiAutomation.executeShellCommand("input keyevent KEYCODE_MENU");
+        sUiAutomation.executeShellCommand("input keyevent KEYCODE_BACK");
     }
 }
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 8da5021..ad09feb 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -424,6 +424,13 @@
 }
 
 flag {
+    name: "screenshot_shelf_ui"
+    namespace: "systemui"
+    description: "Use new shelf UI flow for screenshots"
+    bug: "329659738"
+}
+
+flag {
    name: "run_fingerprint_detect_on_dismissible_keyguard"
    namespace: "systemui"
    description: "Run fingerprint detect instead of authenticate if the keyguard is dismissible."
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
index 82e19e7c..1d86b15 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
@@ -58,7 +58,6 @@
         if (currentClock?.smallClock?.view == null) {
             return
         }
-        viewModel.clock = currentClock
 
         val context = LocalContext.current
         MovableElement(key = smallClockElementKey, modifier = modifier) {
@@ -89,7 +88,6 @@
     @Composable
     fun SceneScope.LargeClock(modifier: Modifier = Modifier) {
         val currentClock by viewModel.currentClock.collectAsState()
-        viewModel.clock = currentClock
         if (currentClock?.largeClock?.view == null) {
             return
         }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
index 5c9b271..525ad16 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
@@ -16,45 +16,33 @@
 
 package com.android.systemui.keyguard.ui.composable.section
 
-import android.content.Context
 import android.view.ViewGroup
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import com.android.compose.animation.scene.SceneScope
 import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.notifications.ui.composable.NotificationStack
 import com.android.systemui.scene.shared.flag.SceneContainerFlags
-import com.android.systemui.statusbar.notification.stack.AmbientState
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
-import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder
+import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackViewBinder
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
-import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
 import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
 
 @SysUISingleton
 class NotificationSection
 @Inject
 constructor(
-    @Application private val context: Context,
     private val viewModel: NotificationsPlaceholderViewModel,
-    controller: NotificationStackScrollLayoutController,
     sceneContainerFlags: SceneContainerFlags,
     sharedNotificationContainer: SharedNotificationContainer,
     sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
     stackScrollLayout: NotificationStackScrollLayout,
-    notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel,
-    ambientState: AmbientState,
-    notificationStackSizeCalculator: NotificationStackSizeCalculator,
-    @Main private val mainImmediateDispatcher: CoroutineDispatcher,
+    sharedNotificationContainerBinder: SharedNotificationContainerBinder,
+    notificationStackViewBinder: NotificationStackViewBinder,
 ) {
 
     init {
@@ -73,24 +61,13 @@
             sharedNotificationContainer.addNotificationStackScrollLayout(stackScrollLayout)
         }
 
-        SharedNotificationContainerBinder.bind(
+        sharedNotificationContainerBinder.bind(
             sharedNotificationContainer,
             sharedNotificationContainerViewModel,
-            sceneContainerFlags,
-            controller,
-            notificationStackSizeCalculator,
-            mainImmediateDispatcher = mainImmediateDispatcher,
         )
 
         if (sceneContainerFlags.isEnabled()) {
-            NotificationStackAppearanceViewBinder.bind(
-                context,
-                sharedNotificationContainer,
-                notificationStackAppearanceViewModel,
-                ambientState,
-                controller,
-                mainImmediateDispatcher = mainImmediateDispatcher,
-            )
+            notificationStackViewBinder.bindWhileAttached()
         }
     }
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index 8ad2bb7..9ba5e3b 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -57,6 +57,7 @@
 import androidx.compose.ui.layout.positionInWindow
 import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.dimensionResource
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
@@ -70,10 +71,10 @@
 import com.android.systemui.notifications.ui.composable.Notifications.Form
 import com.android.systemui.notifications.ui.composable.Notifications.TransitionThresholds.EXPANSION_FOR_MAX_CORNER_RADIUS
 import com.android.systemui.notifications.ui.composable.Notifications.TransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA
+import com.android.systemui.res.R
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.ui.composable.ShadeHeader
 import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding
-import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder.SCRIM_CORNER_RADIUS
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
 import kotlin.math.roundToInt
 
@@ -140,6 +141,7 @@
 ) {
     val density = LocalDensity.current
     val screenCornerRadius = LocalScreenCornerRadius.current
+    val scrimCornerRadius = dimensionResource(R.dimen.notification_scrim_corner_radius)
     val scrollState = rememberScrollState()
     val syntheticScroll = viewModel.syntheticScroll.collectAsState(0f)
     val expansionFraction by viewModel.expandFraction.collectAsState(0f)
@@ -157,7 +159,7 @@
 
     val contentHeight = viewModel.intrinsicContentHeight.collectAsState()
 
-    val stackRounding = viewModel.stackRounding.collectAsState()
+    val stackRounding = viewModel.stackRounding.collectAsState(StackRounding())
 
     // the offset for the notifications scrim. Its upper bound is 0, and its lower bound is
     // calculated in minScrimOffset. The scrim is the same height as the screen minus the
@@ -225,6 +227,7 @@
                 .graphicsLayer {
                     shape =
                         calculateCornerRadius(
+                                scrimCornerRadius,
                                 screenCornerRadius,
                                 { expansionFraction },
                                 layoutState.isTransitioningBetween(Scenes.Gone, Scenes.Shade)
@@ -357,6 +360,7 @@
 }
 
 private fun calculateCornerRadius(
+    scrimCornerRadius: Dp,
     screenCornerRadius: Dp,
     expansionFraction: () -> Float,
     transitioning: Boolean,
@@ -364,12 +368,12 @@
     return if (transitioning) {
         lerp(
                 start = screenCornerRadius.value,
-                stop = SCRIM_CORNER_RADIUS,
+                stop = scrimCornerRadius.value,
                 fraction = (expansionFraction() / EXPANSION_FOR_MAX_CORNER_RADIUS).coerceIn(0f, 1f),
             )
             .dp
     } else {
-        SCRIM_CORNER_RADIUS.dp
+        scrimCornerRadius
     }
 }
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index 2c31f9b..85798ac 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -370,7 +370,8 @@
                 NotificationScrollingStack(
                     viewModel = viewModel.notifications,
                     maxScrimTop = { 0f },
-                    modifier = Modifier.weight(1f).fillMaxHeight(),
+                    modifier =
+                        Modifier.weight(1f).fillMaxHeight().padding(bottom = navBarBottomHeight),
                 )
             }
         }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt
index 6dd425c..e3fa89c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt
@@ -20,9 +20,12 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds
+import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding
+import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
@@ -30,10 +33,9 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-@android.platform.test.annotations.EnabledOnRavenwood
 class NotificationStackAppearanceInteractorTest : SysuiTestCase() {
 
-    private val kosmos = Kosmos()
+    private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val underTest = kosmos.notificationStackAppearanceInteractor
 
@@ -59,6 +61,18 @@
             assertThat(stackBounds).isEqualTo(bounds2)
         }
 
+    @Test
+    fun stackRounding() =
+        testScope.runTest {
+            val stackRounding by collectLastValue(underTest.stackRounding)
+
+            kosmos.shadeRepository.setShadeMode(ShadeMode.Single)
+            assertThat(stackRounding).isEqualTo(StackRounding(roundTop = true, roundBottom = false))
+
+            kosmos.shadeRepository.setShadeMode(ShadeMode.Split)
+            assertThat(stackRounding).isEqualTo(StackRounding(roundTop = true, roundBottom = true))
+        }
+
     @Test(expected = IllegalStateException::class)
     fun setStackBounds_withImproperBounds_throwsException() =
         testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/DisposableHandlesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/DisposableHandlesTest.kt
new file mode 100644
index 0000000..a5ad3c3
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/DisposableHandlesTest.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.util.kotlin
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.DisposableHandle
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DisposableHandlesTest : SysuiTestCase() {
+    @Test
+    fun disposeWorksOnce() {
+        var handleDisposeCount = 0
+        val underTest = DisposableHandles()
+
+        // Add a handle
+        underTest += DisposableHandle { handleDisposeCount++ }
+
+        // dispose() calls dispose() on children
+        underTest.dispose()
+        assertThat(handleDisposeCount).isEqualTo(1)
+
+        // Once disposed, children are not disposed again
+        underTest.dispose()
+        assertThat(handleDisposeCount).isEqualTo(1)
+    }
+
+    @Test
+    fun replaceCallsDispose() {
+        var handleDisposeCount1 = 0
+        var handleDisposeCount2 = 0
+        val underTest = DisposableHandles()
+        val handle1 = DisposableHandle { handleDisposeCount1++ }
+        val handle2 = DisposableHandle { handleDisposeCount2++ }
+
+        // First add handle1
+        underTest += handle1
+
+        // replace() calls dispose() on existing children
+        underTest.replaceAll(handle2)
+        assertThat(handleDisposeCount1).isEqualTo(1)
+        assertThat(handleDisposeCount2).isEqualTo(0)
+
+        // Once disposed, replaced children are not disposed again
+        underTest.dispose()
+        assertThat(handleDisposeCount1).isEqualTo(1)
+        assertThat(handleDisposeCount2).isEqualTo(1)
+    }
+}
diff --git a/packages/SystemUI/res/layout/screenshot_shelf.xml b/packages/SystemUI/res/layout/screenshot_shelf.xml
new file mode 100644
index 0000000..ef1a21f
--- /dev/null
+++ b/packages/SystemUI/res/layout/screenshot_shelf.xml
@@ -0,0 +1,160 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<com.android.systemui.screenshot.ui.ScreenshotShelfView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <ImageView
+        android:id="@+id/actions_container_background"
+        android:visibility="gone"
+        android:layout_height="0dp"
+        android:layout_width="0dp"
+        android:elevation="4dp"
+        android:background="@drawable/action_chip_container_background"
+        android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal"
+        android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="@+id/actions_container"
+        app:layout_constraintEnd_toEndOf="@+id/actions_container"
+        app:layout_constraintBottom_toTopOf="@id/guideline"/>
+    <HorizontalScrollView
+        android:id="@+id/actions_container"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal"
+        android:paddingEnd="@dimen/overlay_action_container_padding_end"
+        android:paddingVertical="@dimen/overlay_action_container_padding_vertical"
+        android:elevation="4dp"
+        android:scrollbars="none"
+        app:layout_constraintHorizontal_bias="0"
+        app:layout_constraintWidth_percent="1.0"
+        app:layout_constraintWidth_max="wrap"
+        app:layout_constraintStart_toEndOf="@+id/screenshot_preview_border"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintBottom_toBottomOf="@id/actions_container_background">
+        <LinearLayout
+            android:id="@+id/screenshot_actions"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content">
+            <include layout="@layout/overlay_action_chip"
+                     android:id="@+id/screenshot_share_chip"/>
+            <include layout="@layout/overlay_action_chip"
+                     android:id="@+id/screenshot_edit_chip"/>
+            <include layout="@layout/overlay_action_chip"
+                     android:id="@+id/screenshot_scroll_chip"
+                     android:visibility="gone" />
+        </LinearLayout>
+    </HorizontalScrollView>
+    <View
+        android:id="@+id/screenshot_preview_border"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_marginStart="16dp"
+        android:layout_marginTop="@dimen/overlay_border_width_neg"
+        android:layout_marginEnd="@dimen/overlay_border_width_neg"
+        android:layout_marginBottom="14dp"
+        android:elevation="8dp"
+        android:background="@drawable/overlay_border"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="@id/screenshot_preview"
+        app:layout_constraintEnd_toEndOf="@id/screenshot_preview"
+        app:layout_constraintBottom_toBottomOf="parent"/>
+    <ImageView
+        android:id="@+id/screenshot_preview"
+        android:layout_width="@dimen/overlay_x_scale"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/overlay_border_width"
+        android:layout_marginBottom="@dimen/overlay_border_width"
+        android:layout_gravity="center"
+        android:elevation="8dp"
+        android:contentDescription="@string/screenshot_edit_description"
+        android:scaleType="fitEnd"
+        android:background="@drawable/overlay_preview_background"
+        android:adjustViewBounds="true"
+        android:clickable="true"
+        app:layout_constraintStart_toStartOf="@id/screenshot_preview_border"
+        app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"/>
+    <ImageView
+        android:id="@+id/screenshot_badge"
+        android:layout_width="56dp"
+        android:layout_height="56dp"
+        android:visibility="gone"
+        android:elevation="9dp"
+        app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"
+        app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border"/>
+    <FrameLayout
+        android:id="@+id/screenshot_dismiss_button"
+        android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
+        android:layout_height="@dimen/overlay_dismiss_button_tappable_size"
+        android:elevation="11dp"
+        android:visibility="gone"
+        app:layout_constraintStart_toEndOf="@id/screenshot_preview"
+        app:layout_constraintEnd_toEndOf="@id/screenshot_preview"
+        app:layout_constraintTop_toTopOf="@id/screenshot_preview"
+        app:layout_constraintBottom_toTopOf="@id/screenshot_preview"
+        android:contentDescription="@string/screenshot_dismiss_description">
+        <ImageView
+            android:id="@+id/screenshot_dismiss_image"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_margin="@dimen/overlay_dismiss_button_margin"
+            android:background="@drawable/circular_background"
+            android:backgroundTint="?androidprv:attr/materialColorPrimary"
+            android:tint="?androidprv:attr/materialColorOnPrimary"
+            android:padding="4dp"
+            android:src="@drawable/ic_close"/>
+    </FrameLayout>
+    <ImageView
+        android:id="@+id/screenshot_scrollable_preview"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:scaleType="matrix"
+        android:visibility="gone"
+        app:layout_constraintStart_toStartOf="@id/screenshot_preview"
+        app:layout_constraintTop_toTopOf="@id/screenshot_preview"
+        android:elevation="7dp"/>
+
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/guideline"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        app:layout_constraintGuide_end="0dp" />
+
+    <FrameLayout
+        android:id="@+id/screenshot_message_container"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="@dimen/overlay_action_container_margin_horizontal"
+        android:layout_marginTop="4dp"
+        android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
+        android:paddingHorizontal="@dimen/overlay_action_container_padding_end"
+        android:paddingVertical="@dimen/overlay_action_container_padding_vertical"
+        android:elevation="4dp"
+        android:background="@drawable/action_chip_container_background"
+        android:visibility="gone"
+        app:layout_constraintTop_toBottomOf="@id/guideline"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintWidth_max="450dp"
+        app:layout_constraintHorizontal_bias="0">
+        <include layout="@layout/screenshot_work_profile_first_run" />
+        <include layout="@layout/screenshot_detection_notice" />
+    </FrameLayout>
+</com.android.systemui.screenshot.ui.ScreenshotShelfView>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index bf5eeb9..3029888 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -235,6 +235,8 @@
     <string name="screenshot_edit_label">Edit</string>
     <!-- Content description indicating that tapping the element will allow editing the screenshot [CHAR LIMIT=NONE] -->
     <string name="screenshot_edit_description">Edit screenshot</string>
+    <!-- Label for UI element which allows sharing the screenshot [CHAR LIMIT=30] -->
+    <string name="screenshot_share_label">Share</string>
     <!-- Content description indicating that tapping the element will allow sharing the screenshot [CHAR LIMIT=NONE] -->
     <string name="screenshot_share_description">Share screenshot</string>
     <!-- Label for UI element which allows the user to capture additional off-screen content in a screenshot. [CHAR LIMIT=30] -->
@@ -2011,6 +2013,10 @@
     <string name="system_multitasking_lhs">Enter split screen with current app to LHS</string>
     <!-- User visible title for the keyboard shortcut that switches from split screen to full screen [CHAR LIMIT=70] -->
     <string name="system_multitasking_full_screen">Switch from split screen to full screen</string>
+    <!-- User visible title for the keyboard shortcut that switches to app on right or below while using split screen [CHAR LIMIT=70] -->
+    <string name="system_multitasking_splitscreen_focus_rhs">Switch to app on right or below while using split screen</string>
+    <!-- User visible title for the keyboard shortcut that switches to app on left or above while using split screen [CHAR LIMIT=70] -->
+    <string name="system_multitasking_splitscreen_focus_lhs">Switch to app on left or above while using split screen</string>
     <!-- User visible title for the keyboard shortcut that replaces an app from one to another during split screen [CHAR LIMIT=70] -->
     <string name="system_multitasking_replace">During split screen: replace an app from one to another</string>
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index e621ffe..86f64f8 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -631,7 +631,7 @@
     @Nullable
     public ClockController getClock() {
         if (migrateClocksToBlueprint()) {
-            return mKeyguardClockInteractor.getClock();
+            return mKeyguardClockInteractor.getCurrentClock().getValue();
         } else {
             return mClockEventController.getClock();
         }
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt
index 964eb6f..578389b 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt
@@ -54,6 +54,18 @@
     }
 
     /**
+     * Returns a [Flow] that emits a dimension pixel size that is kept in sync with the device
+     * configuration.
+     *
+     * @see android.content.res.Resources.getDimensionPixelSize
+     */
+    fun getDimensionPixelOffset(@DimenRes id: Int): Flow<Int> {
+        return configurationController.onDensityOrFontScaleChanged.emitOnStart().map {
+            context.resources.getDimensionPixelOffset(id)
+        }
+    }
+
+    /**
      * Returns a [Flow] that emits a color that is kept in sync with the device theme.
      *
      * @see Utils.getColorAttrDefaultColor
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt
index 7ad5aac..7a56554 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt
@@ -113,7 +113,10 @@
 
     override val currentClock: StateFlow<ClockController?> =
         currentClockId
-            .map { clockRegistry.createCurrentClock() }
+            .map {
+                clockEventController.clock = clockRegistry.createCurrentClock()
+                clockEventController.clock
+            }
             .stateIn(
                 scope = applicationScope,
                 started = SharingStarted.WhileSubscribed(),
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
index 4812e03..89148b0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
@@ -213,9 +213,10 @@
         cs: ConstraintSet,
         viewModel: KeyguardClockViewModel
     ) {
-        if (!DEBUG || viewModel.clock == null) return
+        val currentClock = viewModel.currentClock.value
+        if (!DEBUG || currentClock == null) return
         val smallClockViewId = R.id.lockscreen_clock_view
-        val largeClockViewId = viewModel.clock!!.largeClock.layout.views[0].id
+        val largeClockViewId = currentClock.largeClock.layout.views[0].id
         Log.i(
             TAG,
             "applyCsToSmallClock: vis=${cs.getVisibility(smallClockViewId)} " +
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
index 01596ed..fa1fe5e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
@@ -19,6 +19,7 @@
 import android.transition.TransitionManager
 import android.transition.TransitionSet
 import android.view.View.INVISIBLE
+import android.view.ViewGroup
 import androidx.annotation.VisibleForTesting
 import androidx.constraintlayout.helper.widget.Layer
 import androidx.constraintlayout.widget.ConstraintLayout
@@ -40,7 +41,8 @@
 
 object KeyguardClockViewBinder {
     private val TAG = KeyguardClockViewBinder::class.simpleName!!
-
+    // When changing to new clock, we need to remove old clock views from burnInLayer
+    private var lastClock: ClockController? = null
     @JvmStatic
     fun bind(
         clockSection: ClockSection,
@@ -55,12 +57,11 @@
             }
         }
         keyguardRootView.repeatWhenAttached {
-            repeatOnLifecycle(Lifecycle.State.STARTED) {
+            repeatOnLifecycle(Lifecycle.State.CREATED) {
                 launch {
                     if (!migrateClocksToBlueprint()) return@launch
                     viewModel.currentClock.collect { currentClock ->
-                        cleanupClockViews(viewModel.clock, keyguardRootView, viewModel.burnInLayer)
-                        viewModel.clock = currentClock
+                        cleanupClockViews(currentClock, keyguardRootView, viewModel.burnInLayer)
                         addClockViews(currentClock, keyguardRootView)
                         updateBurnInLayer(keyguardRootView, viewModel)
                         applyConstraints(clockSection, keyguardRootView, true)
@@ -76,7 +77,7 @@
                 launch {
                     if (!migrateClocksToBlueprint()) return@launch
                     viewModel.clockShouldBeCentered.collect { clockShouldBeCentered ->
-                        viewModel.clock?.let {
+                        viewModel.currentClock.value?.let {
                             // Weather clock also has hasCustomPositionUpdatedAnimation as true
                             // TODO(b/323020908): remove ID check
                             if (
@@ -93,7 +94,7 @@
                 launch {
                     if (!migrateClocksToBlueprint()) return@launch
                     viewModel.isAodIconsVisible.collect { isAodIconsVisible ->
-                        viewModel.clock?.let {
+                        viewModel.currentClock.value?.let {
                             // Weather clock also has hasCustomPositionUpdatedAnimation as true
                             if (
                                 viewModel.useLargeClock && it.config.id == "DIGITAL_CLOCK_WEATHER"
@@ -132,11 +133,14 @@
     }
 
     private fun cleanupClockViews(
-        clockController: ClockController?,
+        currentClock: ClockController?,
         rootView: ConstraintLayout,
         burnInLayer: Layer?
     ) {
-        clockController?.let { clock ->
+        if (lastClock == currentClock) {
+            return
+        }
+        lastClock?.let { clock ->
             clock.smallClock.layout.views.forEach {
                 burnInLayer?.removeView(it)
                 rootView.removeView(it)
@@ -150,6 +154,7 @@
             }
             clock.largeClock.layout.views.forEach { rootView.removeView(it) }
         }
+        lastClock = currentClock
     }
 
     @VisibleForTesting
@@ -157,11 +162,19 @@
         clockController: ClockController?,
         rootView: ConstraintLayout,
     ) {
+        // We'll collect the same clock when exiting wallpaper picker without changing clock
+        // so we need to remove clock views from parent before addView again
         clockController?.let { clock ->
             clock.smallClock.layout.views.forEach {
+                if (it.parent != null) {
+                    (it.parent as ViewGroup).removeView(it)
+                }
                 rootView.addView(it).apply { it.visibility = INVISIBLE }
             }
             clock.largeClock.layout.views.forEach {
+                if (it.parent != null) {
+                    (it.parent as ViewGroup).removeView(it)
+                }
                 rootView.addView(it).apply { it.visibility = INVISIBLE }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
index b77f0c5..4d0a25f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
@@ -41,7 +41,7 @@
         blueprintInteractor: KeyguardBlueprintInteractor,
     ) {
         keyguardRootView.repeatWhenAttached {
-            repeatOnLifecycle(Lifecycle.State.STARTED) {
+            repeatOnLifecycle(Lifecycle.State.CREATED) {
                 launch {
                     if (!migrateClocksToBlueprint()) return@launch
                     clockViewModel.hasCustomWeatherDataDisplay.collect { hasCustomWeatherDataDisplay
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index 7c76e6a..f60da0e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -90,6 +90,7 @@
 import com.android.systemui.statusbar.phone.KeyguardBottomAreaView
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
 import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
+import com.android.systemui.util.kotlin.DisposableHandles
 import com.android.systemui.util.settings.SecureSettings
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedInject
@@ -173,7 +174,7 @@
     private lateinit var smallClockHostView: FrameLayout
     private var smartSpaceView: View? = null
 
-    private val disposables = mutableSetOf<DisposableHandle>()
+    private val disposables = DisposableHandles()
     private var isDestroyed = false
 
     private val shortcutsBindings = mutableSetOf<KeyguardQuickAffordanceViewBinder.Binding>()
@@ -183,7 +184,7 @@
 
     init {
         coroutineScope = CoroutineScope(applicationScope.coroutineContext + Job())
-        disposables.add(DisposableHandle { coroutineScope.cancel() })
+        disposables += DisposableHandle { coroutineScope.cancel() }
 
         if (keyguardBottomAreaRefactor()) {
             quickAffordancesCombinedViewModel.enablePreviewMode(
@@ -214,7 +215,7 @@
                     if (hostToken == null) null else InputTransferToken(hostToken),
                     "KeyguardPreviewRenderer"
                 )
-            disposables.add(DisposableHandle { host.release() })
+            disposables += DisposableHandle { host.release() }
         }
     }
 
@@ -284,7 +285,7 @@
     fun destroy() {
         isDestroyed = true
         lockscreenSmartspaceController.disconnect()
-        disposables.forEach { it.dispose() }
+        disposables.dispose()
         if (keyguardBottomAreaRefactor()) {
             shortcutsBindings.forEach { it.destroy() }
         }
@@ -372,7 +373,7 @@
     private fun setupKeyguardRootView(previewContext: Context, rootView: FrameLayout) {
         val keyguardRootView = KeyguardRootView(previewContext, null)
         if (!keyguardBottomAreaRefactor()) {
-            disposables.add(
+            disposables +=
                 KeyguardRootViewBinder.bind(
                     keyguardRootView,
                     keyguardRootViewModel,
@@ -387,7 +388,6 @@
                     null, // device entry haptics not required for preview mode
                     null, // falsing manager not required for preview mode
                 )
-            )
         }
         rootView.addView(
             keyguardRootView,
@@ -555,14 +555,12 @@
                     }
                 }
             clockRegistry.registerClockChangeListener(clockChangeListener)
-            disposables.add(
-                DisposableHandle {
-                    clockRegistry.unregisterClockChangeListener(clockChangeListener)
-                }
-            )
+            disposables += DisposableHandle {
+                clockRegistry.unregisterClockChangeListener(clockChangeListener)
+            }
 
             clockController.registerListeners(parentView)
-            disposables.add(DisposableHandle { clockController.unregisterListeners() })
+            disposables += DisposableHandle { clockController.unregisterListeners() }
         }
 
         val receiver =
@@ -581,7 +579,7 @@
                 addAction(Intent.ACTION_TIME_CHANGED)
             },
         )
-        disposables.add(DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) })
+        disposables += DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) }
 
         if (!migrateClocksToBlueprint()) {
             val layoutChangeListener =
@@ -602,9 +600,9 @@
                     }
                 }
             parentView.addOnLayoutChangeListener(layoutChangeListener)
-            disposables.add(
-                DisposableHandle { parentView.removeOnLayoutChangeListener(layoutChangeListener) }
-            )
+            disposables += DisposableHandle {
+                parentView.removeOnLayoutChangeListener(layoutChangeListener)
+            }
         }
 
         onClockChanged()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt
index 9c9df80..a215efa 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt
@@ -41,7 +41,7 @@
 
     private fun excludeClockAndSmartspaceViews(transition: Transition) {
         transition.excludeTarget(SmartspaceView::class.java, true)
-        clockViewModel.clock?.let { clock ->
+        clockViewModel.currentClock.value?.let { clock ->
             clock.largeClock.layout.views.forEach { view -> transition.excludeTarget(view, true) }
             clock.smallClock.layout.views.forEach { view -> transition.excludeTarget(view, true) }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
index 3adeb2a..c69d868 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
@@ -57,7 +57,9 @@
         when (config.type) {
             Type.NoTransition -> {}
             Type.DefaultClockStepping ->
-                addTransition(clockViewModel.clock?.let { DefaultClockSteppingTransition(it) })
+                addTransition(
+                    clockViewModel.currentClock.value?.let { DefaultClockSteppingTransition(it) }
+                )
             else -> addTransition(ClockSizeTransition(config, clockViewModel, smartspaceViewModel))
         }
     }
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 a183b72..7847c1c 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
@@ -86,7 +86,7 @@
         if (!Flags.migrateClocksToBlueprint()) {
             return
         }
-        clockInteractor.clock?.let { clock ->
+        keyguardClockViewModel.currentClock.value?.let { clock ->
             constraintSet.applyDeltaFrom(buildConstraints(clock, constraintSet))
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
index 6a3b920..c1b0cc6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
@@ -26,20 +26,16 @@
 import androidx.constraintlayout.widget.ConstraintSet.TOP
 import com.android.systemui.Flags.centralizedStatusBarHeightFix
 import com.android.systemui.Flags.migrateClocksToBlueprint
-import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.res.R
 import com.android.systemui.scene.shared.flag.SceneContainerFlags
 import com.android.systemui.shade.LargeScreenHeaderHelper
 import com.android.systemui.shade.NotificationPanelView
-import com.android.systemui.statusbar.notification.stack.AmbientState
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
-import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
+import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackViewBinder
+import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
 import dagger.Lazy
 import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
 
 /** Single column format for notifications (default for phones) */
 class DefaultNotificationStackScrollLayoutSection
@@ -50,12 +46,9 @@
     notificationPanelView: NotificationPanelView,
     sharedNotificationContainer: SharedNotificationContainer,
     sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
-    notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel,
-    ambientState: AmbientState,
-    controller: NotificationStackScrollLayoutController,
-    notificationStackSizeCalculator: NotificationStackSizeCalculator,
+    sharedNotificationContainerBinder: SharedNotificationContainerBinder,
+    notificationStackViewBinder: NotificationStackViewBinder,
     private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>,
-    @Main mainDispatcher: CoroutineDispatcher,
 ) :
     NotificationStackScrollLayoutSection(
         context,
@@ -63,11 +56,8 @@
         notificationPanelView,
         sharedNotificationContainer,
         sharedNotificationContainerViewModel,
-        notificationStackAppearanceViewModel,
-        ambientState,
-        controller,
-        notificationStackSizeCalculator,
-        mainDispatcher,
+        sharedNotificationContainerBinder,
+        notificationStackViewBinder,
     ) {
     override fun applyConstraints(constraintSet: ConstraintSet) {
         if (!migrateClocksToBlueprint()) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
index 5dea7cb..8323502 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
@@ -31,16 +31,11 @@
 import com.android.systemui.res.R
 import com.android.systemui.scene.shared.flag.SceneContainerFlags
 import com.android.systemui.shade.NotificationPanelView
-import com.android.systemui.statusbar.notification.stack.AmbientState
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
-import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder
+import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackViewBinder
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
-import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.DisposableHandle
+import com.android.systemui.util.kotlin.DisposableHandles
 
 abstract class NotificationStackScrollLayoutSection
 constructor(
@@ -49,14 +44,11 @@
     private val notificationPanelView: NotificationPanelView,
     private val sharedNotificationContainer: SharedNotificationContainer,
     private val sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
-    private val notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel,
-    private val ambientState: AmbientState,
-    private val controller: NotificationStackScrollLayoutController,
-    private val notificationStackSizeCalculator: NotificationStackSizeCalculator,
-    private val mainDispatcher: CoroutineDispatcher,
+    private val sharedNotificationContainerBinder: SharedNotificationContainerBinder,
+    private val notificationStackViewBinder: NotificationStackViewBinder,
 ) : KeyguardSection() {
     private val placeHolderId = R.id.nssl_placeholder
-    private val disposableHandles: MutableList<DisposableHandle> = mutableListOf()
+    private val disposableHandles = DisposableHandles()
 
     /**
      * Align the notification placeholder bottom to the top of either the lock icon or the ambient
@@ -102,39 +94,20 @@
             return
         }
 
-        disposeHandles()
-        disposableHandles.add(
-            SharedNotificationContainerBinder.bind(
+        disposableHandles.dispose()
+        disposableHandles +=
+            sharedNotificationContainerBinder.bind(
                 sharedNotificationContainer,
                 sharedNotificationContainerViewModel,
-                sceneContainerFlags,
-                controller,
-                notificationStackSizeCalculator,
-                mainImmediateDispatcher = mainDispatcher,
             )
-        )
 
         if (sceneContainerFlags.isEnabled()) {
-            disposableHandles.add(
-                NotificationStackAppearanceViewBinder.bind(
-                    context,
-                    sharedNotificationContainer,
-                    notificationStackAppearanceViewModel,
-                    ambientState,
-                    controller,
-                    mainImmediateDispatcher = mainDispatcher,
-                )
-            )
+            disposableHandles += notificationStackViewBinder.bindWhileAttached()
         }
     }
 
     override fun removeViews(constraintLayout: ConstraintLayout) {
-        disposeHandles()
+        disposableHandles.dispose()
         constraintLayout.removeView(placeHolderId)
     }
-
-    private fun disposeHandles() {
-        disposableHandles.forEach { it.dispose() }
-        disposableHandles.clear()
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
index 2545302..4a705a7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
@@ -24,19 +24,14 @@
 import androidx.constraintlayout.widget.ConstraintSet.START
 import androidx.constraintlayout.widget.ConstraintSet.TOP
 import com.android.systemui.Flags.migrateClocksToBlueprint
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
 import com.android.systemui.res.R
 import com.android.systemui.scene.shared.flag.SceneContainerFlags
 import com.android.systemui.shade.NotificationPanelView
-import com.android.systemui.statusbar.notification.stack.AmbientState
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
-import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
+import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackViewBinder
+import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
 import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
 
 /** Large-screen format for notifications, shown as two columns on the device */
 class SplitShadeNotificationStackScrollLayoutSection
@@ -47,12 +42,8 @@
     notificationPanelView: NotificationPanelView,
     sharedNotificationContainer: SharedNotificationContainer,
     sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
-    notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel,
-    ambientState: AmbientState,
-    controller: NotificationStackScrollLayoutController,
-    notificationStackSizeCalculator: NotificationStackSizeCalculator,
-    private val smartspaceViewModel: KeyguardSmartspaceViewModel,
-    @Main mainDispatcher: CoroutineDispatcher,
+    sharedNotificationContainerBinder: SharedNotificationContainerBinder,
+    notificationStackViewBinder: NotificationStackViewBinder,
 ) :
     NotificationStackScrollLayoutSection(
         context,
@@ -60,11 +51,8 @@
         notificationPanelView,
         sharedNotificationContainer,
         sharedNotificationContainerViewModel,
-        notificationStackAppearanceViewModel,
-        ambientState,
-        controller,
-        notificationStackSizeCalculator,
-        mainDispatcher,
+        sharedNotificationContainerBinder,
+        notificationStackViewBinder,
     ) {
     override fun applyConstraints(constraintSet: ConstraintSet) {
         if (!migrateClocksToBlueprint()) {
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 6184c82..4d3a78d 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
@@ -216,7 +216,9 @@
             captureSmartspace = !viewModel.useLargeClock && smartspaceViewModel.isSmartspaceEnabled
 
             if (viewModel.useLargeClock) {
-                viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } }
+                viewModel.currentClock.value?.let {
+                    it.largeClock.layout.views.forEach { addTarget(it) }
+                }
             } else {
                 addTarget(R.id.lockscreen_clock_view)
             }
@@ -276,7 +278,9 @@
             if (viewModel.useLargeClock) {
                 addTarget(R.id.lockscreen_clock_view)
             } else {
-                viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } }
+                viewModel.currentClock.value?.let {
+                    it.largeClock.layout.views.forEach { addTarget(it) }
+                }
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
index f961e08..9c1f077 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
@@ -169,7 +169,7 @@
         provider: Provider<ClockController>?,
     ): Provider<ClockController>? {
         return if (Flags.migrateClocksToBlueprint()) {
-            Provider { keyguardClockViewModel.clock }
+            Provider { keyguardClockViewModel.currentClock.value }
         } else {
             provider
         }
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 b6622e5..1c1c33a 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
@@ -26,7 +26,6 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.shared.ComposeLockscreen
 import com.android.systemui.keyguard.shared.model.SettingsClockSize
-import com.android.systemui.plugins.clocks.ClockController
 import com.android.systemui.res.R
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.shared.model.ShadeMode
@@ -54,8 +53,6 @@
     val useLargeClock: Boolean
         get() = clockSize.value == LARGE
 
-    var clock: ClockController? by keyguardClockInteractor::clock
-
     val clockSize =
         combine(keyguardClockInteractor.selectedClockSize, keyguardClockInteractor.clockSize) {
                 selectedSize,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
index 5d2aeef..b34b370 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
@@ -432,6 +432,9 @@
         for (int i = 0; i < NP; i++) {
             mPages.get(i).removeAllViews();
         }
+        if (mPageIndicator != null) {
+            mPageIndicator.setNumPages(numPages);
+        }
         if (NP == numPages) {
             return;
         }
@@ -443,7 +446,6 @@
             mLogger.d("Removing page");
             mPages.remove(mPages.size() - 1);
         }
-        mPageIndicator.setNumPages(mPages.size());
         setAdapter(mAdapter);
         mAdapter.notifyDataSetChanged();
         if (mPageToRestore != NO_PAGE) {
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
index 7313a49..832fc3f 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.recordissue
 
 import android.annotation.SuppressLint
+import android.app.AlertDialog
 import android.content.Context
 import android.content.res.ColorStateList
 import android.graphics.Color
@@ -74,7 +75,6 @@
 
     @SuppressLint("UseSwitchCompatOrMaterialCode") private lateinit var screenRecordSwitch: Switch
     private lateinit var issueTypeButton: Button
-    private var hasSelectedIssueType: Boolean = false
 
     @MainThread
     override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
@@ -86,15 +86,13 @@
             setPositiveButton(
                 R.string.qs_record_issue_start,
                 { _, _ ->
-                    if (hasSelectedIssueType) {
-                        onStarted.accept(
-                            IssueRecordingConfig(
-                                screenRecordSwitch.isChecked,
-                                true /* TODO: Base this on issueType selected */
-                            )
+                    onStarted.accept(
+                        IssueRecordingConfig(
+                            screenRecordSwitch.isChecked,
+                            true /* TODO: Base this on issueType selected */
                         )
-                        dismiss()
-                    }
+                    )
+                    dismiss()
                 },
                 false
             )
@@ -115,8 +113,12 @@
                     bgExecutor.execute { onScreenRecordSwitchClicked() }
                 }
             }
+            val startButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
             issueTypeButton = requireViewById(R.id.issue_type_button)
-            issueTypeButton.setOnClickListener { onIssueTypeClicked(context) }
+            issueTypeButton.setOnClickListener {
+                onIssueTypeClicked(context) { startButton.isEnabled = true }
+            }
+            startButton.isEnabled = false
         }
     }
 
@@ -159,7 +161,7 @@
     }
 
     @MainThread
-    private fun onIssueTypeClicked(context: Context) {
+    private fun onIssueTypeClicked(context: Context, onIssueTypeSelected: Runnable) {
         val selectedCategory = issueTypeButton.text.toString()
         val popupMenu = PopupMenu(context, issueTypeButton)
 
@@ -174,11 +176,11 @@
         popupMenu.apply {
             setOnMenuItemClickListener {
                 issueTypeButton.text = it.title
+                onIssueTypeSelected.run()
                 true
             }
             setForceShowIcon(true)
             show()
         }
-        hasSelectedIssueType = true
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt
new file mode 100644
index 0000000..abdbd68
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot
+
+import android.content.Context
+import android.content.Intent
+import android.graphics.drawable.Drawable
+import android.net.Uri
+import android.os.UserHandle
+import androidx.appcompat.content.res.AppCompatResources
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+/**
+ * Provides static actions for screenshots. This class can be overridden by a vendor-specific SysUI
+ * implementation.
+ */
+interface ScreenshotActionsProvider {
+    data class ScreenshotAction(
+        val icon: Drawable?,
+        val text: String?,
+        val overrideTransition: Boolean,
+        val retrieveIntent: (Uri) -> Intent
+    )
+
+    fun getPreviewAction(context: Context, uri: Uri, user: UserHandle): Intent
+    fun getActions(context: Context, user: UserHandle): List<ScreenshotAction>
+}
+
+class DefaultScreenshotActionsProvider @Inject constructor() : ScreenshotActionsProvider {
+    override fun getPreviewAction(context: Context, uri: Uri, user: UserHandle): Intent {
+        return ActionIntentCreator.createEdit(uri, context)
+    }
+
+    override fun getActions(
+        context: Context,
+        user: UserHandle
+    ): List<ScreenshotActionsProvider.ScreenshotAction> {
+        val editAction =
+            ScreenshotActionsProvider.ScreenshotAction(
+                AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_edit),
+                context.resources.getString(R.string.screenshot_edit_label),
+                true
+            ) { uri ->
+                ActionIntentCreator.createEdit(uri, context)
+            }
+        val shareAction =
+            ScreenshotActionsProvider.ScreenshotAction(
+                AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_share),
+                context.resources.getString(R.string.screenshot_share_label),
+                false
+            ) { uri ->
+                ActionIntentCreator.createShare(uri)
+            }
+        return listOf(editAction, shareAction)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
new file mode 100644
index 0000000..9354fd2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.app.Notification
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Rect
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.ScrollCaptureResponse
+import android.view.View
+import android.view.ViewTreeObserver
+import android.view.WindowInsets
+import android.window.OnBackInvokedCallback
+import android.window.OnBackInvokedDispatcher
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.log.DebugLogger.debugLog
+import com.android.systemui.res.R
+import com.android.systemui.screenshot.LogConfig.DEBUG_ACTIONS
+import com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS
+import com.android.systemui.screenshot.LogConfig.DEBUG_INPUT
+import com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW
+import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER
+import com.android.systemui.screenshot.scroll.ScrollCaptureController
+import com.android.systemui.screenshot.ui.ScreenshotAnimationController
+import com.android.systemui.screenshot.ui.ScreenshotShelfView
+import com.android.systemui.screenshot.ui.binder.ScreenshotShelfViewBinder
+import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel
+import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+/** Controls the screenshot view and viewModel. */
+class ScreenshotShelfViewProxy
+@AssistedInject
+constructor(
+    private val logger: UiEventLogger,
+    private val viewModel: ScreenshotViewModel,
+    private val staticActionsProvider: ScreenshotActionsProvider,
+    @Assisted private val context: Context,
+    @Assisted private val displayId: Int
+) : ScreenshotViewProxy {
+    override val view: ScreenshotShelfView =
+        LayoutInflater.from(context).inflate(R.layout.screenshot_shelf, null) as ScreenshotShelfView
+    override val screenshotPreview: View
+    override var packageName: String = ""
+    override var callbacks: ScreenshotView.ScreenshotViewCallback? = null
+    override var screenshot: ScreenshotData? = null
+        set(value) {
+            viewModel.setScreenshotBitmap(value?.bitmap)
+            field = value
+        }
+
+    override val isAttachedToWindow
+        get() = view.isAttachedToWindow
+    override var isDismissing = false
+    override var isPendingSharedTransition = false
+
+    private val animationController = ScreenshotAnimationController(view)
+
+    init {
+        ScreenshotShelfViewBinder.bind(view, viewModel, LayoutInflater.from(context))
+        addPredictiveBackListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) }
+        setOnKeyListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) }
+        debugLog(DEBUG_WINDOW) { "adding OnComputeInternalInsetsListener" }
+        screenshotPreview = view.screenshotPreview
+    }
+
+    override fun reset() {
+        animationController.cancel()
+        isPendingSharedTransition = false
+        viewModel.setScreenshotBitmap(null)
+        viewModel.setActions(listOf())
+    }
+    override fun updateInsets(insets: WindowInsets) {}
+    override fun updateOrientation(insets: WindowInsets) {}
+
+    override fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator {
+        return animationController.getEntranceAnimation()
+    }
+
+    override fun addQuickShareChip(quickShareAction: Notification.Action) {}
+
+    override fun setChipIntents(imageData: ScreenshotController.SavedImageData) {
+        val staticActions =
+            staticActionsProvider.getActions(context, imageData.owner).map {
+                ActionButtonViewModel(it.icon, it.text) {
+                    val intent = it.retrieveIntent(imageData.uri)
+                    debugLog(DEBUG_ACTIONS) { "Action tapped: $intent" }
+                    isPendingSharedTransition = true
+                    callbacks?.onAction(intent, imageData.owner, it.overrideTransition)
+                }
+            }
+
+        viewModel.setActions(staticActions)
+    }
+
+    override fun requestDismissal(event: ScreenshotEvent) {
+        debugLog(DEBUG_DISMISS) { "screenshot dismissal requested: $event" }
+
+        // If we're already animating out, don't restart the animation
+        if (isDismissing) {
+            debugLog(DEBUG_DISMISS) { "Already dismissing, ignoring duplicate command $event" }
+            return
+        }
+        logger.log(event, 0, packageName)
+        val animator = animationController.getExitAnimation()
+        animator.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationStart(animator: Animator) {
+                    isDismissing = true
+                }
+                override fun onAnimationEnd(animator: Animator) {
+                    isDismissing = false
+                    callbacks?.onDismiss()
+                }
+            }
+        )
+        animator.start()
+    }
+
+    override fun showScrollChip(packageName: String, onClick: Runnable) {}
+
+    override fun hideScrollChip() {}
+
+    override fun prepareScrollingTransition(
+        response: ScrollCaptureResponse,
+        screenBitmap: Bitmap,
+        newScreenshot: Bitmap,
+        screenshotTakenInPortrait: Boolean,
+        onTransitionPrepared: Runnable,
+    ) {}
+
+    override fun startLongScreenshotTransition(
+        transitionDestination: Rect,
+        onTransitionEnd: Runnable,
+        longScreenshot: ScrollCaptureController.LongScreenshot
+    ) {}
+
+    override fun restoreNonScrollingUi() {}
+
+    override fun stopInputListening() {}
+
+    override fun requestFocus() {
+        view.requestFocus()
+    }
+
+    override fun announceForAccessibility(string: String) = view.announceForAccessibility(string)
+
+    override fun prepareEntranceAnimation(runnable: Runnable) {
+        view.viewTreeObserver.addOnPreDrawListener(
+            object : ViewTreeObserver.OnPreDrawListener {
+                override fun onPreDraw(): Boolean {
+                    debugLog(DEBUG_WINDOW) { "onPreDraw: startAnimation" }
+                    view.viewTreeObserver.removeOnPreDrawListener(this)
+                    runnable.run()
+                    return true
+                }
+            }
+        )
+    }
+
+    private fun addPredictiveBackListener(onDismissRequested: (ScreenshotEvent) -> Unit) {
+        val onBackInvokedCallback = OnBackInvokedCallback {
+            debugLog(DEBUG_INPUT) { "Predictive Back callback dispatched" }
+            onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER)
+        }
+        view.addOnAttachStateChangeListener(
+            object : View.OnAttachStateChangeListener {
+                override fun onViewAttachedToWindow(v: View) {
+                    debugLog(DEBUG_INPUT) { "Registering Predictive Back callback" }
+                    view
+                        .findOnBackInvokedDispatcher()
+                        ?.registerOnBackInvokedCallback(
+                            OnBackInvokedDispatcher.PRIORITY_DEFAULT,
+                            onBackInvokedCallback
+                        )
+                }
+
+                override fun onViewDetachedFromWindow(view: View) {
+                    debugLog(DEBUG_INPUT) { "Unregistering Predictive Back callback" }
+                    view
+                        .findOnBackInvokedDispatcher()
+                        ?.unregisterOnBackInvokedCallback(onBackInvokedCallback)
+                }
+            }
+        )
+    }
+    private fun setOnKeyListener(onDismissRequested: (ScreenshotEvent) -> Unit) {
+        view.setOnKeyListener(
+            object : View.OnKeyListener {
+                override fun onKey(view: View, keyCode: Int, event: KeyEvent): Boolean {
+                    if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) {
+                        debugLog(DEBUG_INPUT) { "onKeyEvent: $keyCode" }
+                        onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER)
+                        return true
+                    }
+                    return false
+                }
+            }
+        )
+    }
+
+    @AssistedFactory
+    interface Factory : ScreenshotViewProxy.Factory {
+        override fun getProxy(context: Context, displayId: Int): ScreenshotShelfViewProxy
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
index cdb9abb..9118ee1 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
@@ -16,16 +16,23 @@
 
 package com.android.systemui.screenshot.dagger;
 
-import android.app.Service;
+import static com.android.systemui.Flags.screenshotShelfUi;
 
+import android.app.Service;
+import android.view.accessibility.AccessibilityManager;
+
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.screenshot.DefaultScreenshotActionsProvider;
 import com.android.systemui.screenshot.ImageCapture;
 import com.android.systemui.screenshot.ImageCaptureImpl;
 import com.android.systemui.screenshot.LegacyScreenshotViewProxy;
 import com.android.systemui.screenshot.RequestProcessor;
+import com.android.systemui.screenshot.ScreenshotActionsProvider;
 import com.android.systemui.screenshot.ScreenshotPolicy;
 import com.android.systemui.screenshot.ScreenshotPolicyImpl;
 import com.android.systemui.screenshot.ScreenshotProxyService;
 import com.android.systemui.screenshot.ScreenshotRequestProcessor;
+import com.android.systemui.screenshot.ScreenshotShelfViewProxy;
 import com.android.systemui.screenshot.ScreenshotSoundController;
 import com.android.systemui.screenshot.ScreenshotSoundControllerImpl;
 import com.android.systemui.screenshot.ScreenshotSoundProvider;
@@ -34,6 +41,7 @@
 import com.android.systemui.screenshot.TakeScreenshotService;
 import com.android.systemui.screenshot.appclips.AppClipsScreenshotHelperService;
 import com.android.systemui.screenshot.appclips.AppClipsService;
+import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel;
 
 import dagger.Binds;
 import dagger.Module;
@@ -85,9 +93,25 @@
     abstract ScreenshotSoundController bindScreenshotSoundController(
             ScreenshotSoundControllerImpl screenshotSoundProviderImpl);
 
+    @Binds
+    abstract ScreenshotActionsProvider bindScreenshotActionsProvider(
+            DefaultScreenshotActionsProvider defaultScreenshotActionsProvider);
+
+    @Provides
+    @SysUISingleton
+    static ScreenshotViewModel providesScreenshotViewModel(
+            AccessibilityManager accessibilityManager) {
+        return new ScreenshotViewModel(accessibilityManager);
+    }
+
     @Provides
     static ScreenshotViewProxy.Factory providesScreenshotViewProxyFactory(
+            ScreenshotShelfViewProxy.Factory shelfScreenshotViewProxyFactory,
             LegacyScreenshotViewProxy.Factory legacyScreenshotViewProxyFactory) {
-        return legacyScreenshotViewProxyFactory;
+        if (screenshotShelfUi()) {
+            return shelfScreenshotViewProxyFactory;
+        } else {
+            return legacyScreenshotViewProxyFactory;
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt
new file mode 100644
index 0000000..2c17873
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.ui
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.view.View
+
+class ScreenshotAnimationController(private val view: View) {
+    private var animator: Animator? = null
+
+    fun getEntranceAnimation(): Animator {
+        val animator = ValueAnimator.ofFloat(0f, 1f)
+        animator.addUpdateListener { view.alpha = it.animatedFraction }
+        animator.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationStart(animator: Animator) {
+                    view.alpha = 0f
+                }
+                override fun onAnimationEnd(animator: Animator) {
+                    view.alpha = 1f
+                }
+            }
+        )
+        this.animator = animator
+        return animator
+    }
+
+    fun getExitAnimation(): Animator {
+        val animator = ValueAnimator.ofFloat(1f, 0f)
+        animator.addUpdateListener { view.alpha = it.animatedValue as Float }
+        animator.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationStart(animator: Animator) {
+                    view.alpha = 1f
+                }
+                override fun onAnimationEnd(animator: Animator) {
+                    view.alpha = 0f
+                }
+            }
+        )
+        this.animator = animator
+        return animator
+    }
+
+    fun cancel() {
+        animator?.cancel()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt
new file mode 100644
index 0000000..747ad4f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.ui
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.ImageView
+import androidx.constraintlayout.widget.ConstraintLayout
+import com.android.systemui.res.R
+
+class ScreenshotShelfView(context: Context, attrs: AttributeSet? = null) :
+    ConstraintLayout(context, attrs) {
+    lateinit var screenshotPreview: ImageView
+
+    override fun onFinishInflate() {
+        super.onFinishInflate()
+        screenshotPreview = requireViewById(R.id.screenshot_preview)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt
new file mode 100644
index 0000000..a5825b5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.ui.binder
+
+import android.view.View
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import com.android.systemui.res.R
+import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel
+
+object ActionButtonViewBinder {
+    /** Binds the given view to the given view-model */
+    fun bind(view: View, viewModel: ActionButtonViewModel) {
+        val iconView = view.requireViewById<ImageView>(R.id.overlay_action_chip_icon)
+        val textView = view.requireViewById<TextView>(R.id.overlay_action_chip_text)
+        iconView.setImageDrawable(viewModel.icon)
+        textView.text = viewModel.name
+        setMargins(iconView, textView, viewModel.name?.isNotEmpty() ?: false)
+        if (viewModel.onClicked != null) {
+            view.setOnClickListener { viewModel.onClicked.invoke() }
+        } else {
+            view.setOnClickListener(null)
+        }
+        view.visibility = View.VISIBLE
+        view.alpha = 1f
+    }
+
+    private fun setMargins(iconView: View, textView: View, hasText: Boolean) {
+        val iconParams = iconView.layoutParams as LinearLayout.LayoutParams
+        val textParams = textView.layoutParams as LinearLayout.LayoutParams
+        if (hasText) {
+            iconParams.marginStart = iconView.dpToPx(R.dimen.overlay_action_chip_padding_start)
+            iconParams.marginEnd = iconView.dpToPx(R.dimen.overlay_action_chip_spacing)
+            textParams.marginStart = 0
+            textParams.marginEnd = textView.dpToPx(R.dimen.overlay_action_chip_padding_end)
+        } else {
+            val paddingHorizontal =
+                iconView.dpToPx(R.dimen.overlay_action_chip_icon_only_padding_horizontal)
+            iconParams.marginStart = paddingHorizontal
+            iconParams.marginEnd = paddingHorizontal
+        }
+        iconView.layoutParams = iconParams
+        textView.layoutParams = textParams
+    }
+
+    private fun View.dpToPx(dimenId: Int): Int {
+        return this.resources.getDimensionPixelSize(dimenId)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt
new file mode 100644
index 0000000..3bcd52c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.ui.binder
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.LinearLayout
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.res.R
+import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
+import com.android.systemui.util.children
+import kotlinx.coroutines.launch
+
+object ScreenshotShelfViewBinder {
+    fun bind(
+        view: ViewGroup,
+        viewModel: ScreenshotViewModel,
+        layoutInflater: LayoutInflater,
+    ) {
+        val previewView: ImageView = view.requireViewById(R.id.screenshot_preview)
+        val previewBorder = view.requireViewById<View>(R.id.screenshot_preview_border)
+        previewView.clipToOutline = true
+        val actionsContainer: LinearLayout = view.requireViewById(R.id.screenshot_actions)
+        view.requireViewById<View>(R.id.screenshot_dismiss_button).visibility =
+            if (viewModel.showDismissButton) View.VISIBLE else View.GONE
+
+        view.repeatWhenAttached {
+            lifecycleScope.launch {
+                repeatOnLifecycle(Lifecycle.State.STARTED) {
+                    launch {
+                        viewModel.preview.collect { bitmap ->
+                            if (bitmap != null) {
+                                previewView.setImageBitmap(bitmap)
+                                previewView.visibility = View.VISIBLE
+                                previewBorder.visibility = View.VISIBLE
+                            } else {
+                                previewView.visibility = View.GONE
+                                previewBorder.visibility = View.GONE
+                            }
+                        }
+                    }
+                    launch {
+                        viewModel.actions.collect { actions ->
+                            if (actions.isNotEmpty()) {
+                                view
+                                    .requireViewById<View>(R.id.actions_container_background)
+                                    .visibility = View.VISIBLE
+                            }
+                            val viewPool = actionsContainer.children.toList()
+                            actionsContainer.removeAllViews()
+                            val actionButtons =
+                                List(actions.size) {
+                                    viewPool.getOrElse(it) {
+                                        layoutInflater.inflate(
+                                            R.layout.overlay_action_chip,
+                                            actionsContainer,
+                                            false
+                                        )
+                                    }
+                                }
+                            actionButtons.zip(actions).forEach {
+                                actionsContainer.addView(it.first)
+                                ActionButtonViewBinder.bind(it.first, it.second)
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt
new file mode 100644
index 0000000..6ee9705
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.ui.viewmodel
+
+import android.graphics.drawable.Drawable
+
+data class ActionButtonViewModel(
+    val icon: Drawable?,
+    val name: String?,
+    val onClicked: (() -> Unit)?
+)
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt
new file mode 100644
index 0000000..3a652d9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.ui.viewmodel
+
+import android.graphics.Bitmap
+import android.view.accessibility.AccessibilityManager
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+class ScreenshotViewModel(private val accessibilityManager: AccessibilityManager) {
+    private val _preview = MutableStateFlow<Bitmap?>(null)
+    val preview: StateFlow<Bitmap?> = _preview
+    private val _actions = MutableStateFlow(emptyList<ActionButtonViewModel>())
+    val actions: StateFlow<List<ActionButtonViewModel>> = _actions
+    val showDismissButton: Boolean
+        get() = accessibilityManager.isEnabled
+
+    fun setScreenshotBitmap(bitmap: Bitmap?) {
+        _preview.value = bitmap
+    }
+
+    fun setActions(actions: List<ActionButtonViewModel>) {
+        _actions.value = actions
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
index da8c1be..d6858ca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
@@ -631,34 +631,41 @@
         //    Enter Split screen with current app to RHS: Meta + Ctrl + Right arrow
         //    Enter Split screen with current app to LHS: Meta + Ctrl + Left arrow
         //    Switch from Split screen to full screen: Meta + Ctrl + Up arrow
-        String[] shortcutLabels = {
-                context.getString(R.string.system_multitasking_rhs),
-                context.getString(R.string.system_multitasking_lhs),
-                context.getString(R.string.system_multitasking_full_screen),
-        };
-        int[] keyCodes = {
-                KeyEvent.KEYCODE_DPAD_RIGHT,
-                KeyEvent.KEYCODE_DPAD_LEFT,
-                KeyEvent.KEYCODE_DPAD_UP,
-        };
-
-        for (int i = 0; i < shortcutLabels.length; i++) {
-            List<ShortcutKeyGroup> shortcutKeyGroups = Arrays.asList(new ShortcutKeyGroup(
-                    new KeyboardShortcutInfo(
-                            shortcutLabels[i],
-                            keyCodes[i],
-                            KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON),
-                    null));
-            ShortcutMultiMappingInfo shortcutMultiMappingInfo =
-                    new ShortcutMultiMappingInfo(
-                            shortcutLabels[i],
-                            null,
-                            shortcutKeyGroups);
-            systemMultitaskingGroup.addItem(shortcutMultiMappingInfo);
-        }
+        //    Change split screen focus to RHS: Meta + Alt + Right arrow
+        //    Change split screen focus to LHS: Meta + Alt + Left arrow
+        systemMultitaskingGroup.addItem(
+                getMultitaskingShortcut(context.getString(R.string.system_multitasking_rhs),
+                        KeyEvent.KEYCODE_DPAD_RIGHT,
+                        KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON));
+        systemMultitaskingGroup.addItem(
+                getMultitaskingShortcut(context.getString(R.string.system_multitasking_lhs),
+                        KeyEvent.KEYCODE_DPAD_LEFT,
+                        KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON));
+        systemMultitaskingGroup.addItem(
+                getMultitaskingShortcut(context.getString(R.string.system_multitasking_full_screen),
+                        KeyEvent.KEYCODE_DPAD_UP,
+                        KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON));
+        systemMultitaskingGroup.addItem(
+                getMultitaskingShortcut(
+                        context.getString(R.string.system_multitasking_splitscreen_focus_rhs),
+                        KeyEvent.KEYCODE_DPAD_RIGHT,
+                        KeyEvent.META_META_ON | KeyEvent.META_ALT_ON));
+        systemMultitaskingGroup.addItem(
+                getMultitaskingShortcut(
+                        context.getString(R.string.system_multitasking_splitscreen_focus_lhs),
+                        KeyEvent.KEYCODE_DPAD_LEFT,
+                        KeyEvent.META_META_ON | KeyEvent.META_ALT_ON));
         return systemMultitaskingGroup;
     }
 
+    private static ShortcutMultiMappingInfo getMultitaskingShortcut(String shortcutLabel,
+            int keycode, int modifiers) {
+        List<ShortcutKeyGroup> shortcutKeyGroups = Arrays.asList(
+                new ShortcutKeyGroup(new KeyboardShortcutInfo(shortcutLabel, keycode, modifiers),
+                        null));
+        return new ShortcutMultiMappingInfo(shortcutLabel, null, shortcutKeyGroups);
+    }
+
     private static KeyboardShortcutMultiMappingGroup getMultiMappingInputShortcuts(
             Context context) {
         List<ShortcutMultiMappingInfo> shortcutMultiMappingInfoList = Arrays.asList(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt
index 9fffb66..79ba25e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt
@@ -19,7 +19,6 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds
-import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding
 import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
 
@@ -29,10 +28,6 @@
     /** The bounds of the notification stack in the current scene. */
     val stackBounds = MutableStateFlow(StackBounds())
 
-    /** The whether the corners of the notification stack should be rounded */
-    // TODO: replace with the logic from QSController
-    val stackRounding = MutableStateFlow(StackRounding(roundTop = true, roundBottom = false))
-
     /**
      * The height in px of the contents of notification stack. Depending on the number of
      * notifications, this can exceed the space available on screen to show notifications, at which
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
index 5a56ca1..f05d017 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
@@ -18,6 +18,8 @@
 package com.android.systemui.statusbar.notification.stack.domain.interactor
 
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.statusbar.notification.stack.data.repository.NotificationStackAppearanceRepository
 import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds
 import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding
@@ -25,6 +27,9 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
 
 /** An interactor which controls the appearance of the NSSL */
 @SysUISingleton
@@ -32,12 +37,30 @@
 @Inject
 constructor(
     private val repository: NotificationStackAppearanceRepository,
+    shadeInteractor: ShadeInteractor,
 ) {
     /** The bounds of the notification stack in the current scene. */
     val stackBounds: StateFlow<StackBounds> = repository.stackBounds.asStateFlow()
 
+    /**
+     * Whether the stack is expanding from GONE-with-HUN to SHADE
+     *
+     * TODO(b/296118689): implement this to match legacy QSController logic
+     */
+    private val isExpandingFromHeadsUp: Flow<Boolean> = flowOf(false)
+
     /** The rounding of the notification stack. */
-    val stackRounding: StateFlow<StackRounding> = repository.stackRounding.asStateFlow()
+    val stackRounding: Flow<StackRounding> =
+        combine(
+                shadeInteractor.shadeMode,
+                isExpandingFromHeadsUp,
+            ) { shadeMode, isExpandingFromHeadsUp ->
+                StackRounding(
+                    roundTop = !(shadeMode == ShadeMode.Split && isExpandingFromHeadsUp),
+                    roundBottom = shadeMode != ShadeMode.Single,
+                )
+            }
+            .distinctUntilChanged()
 
     /**
      * The height in px of the contents of notification stack. Depending on the number of
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
deleted file mode 100644
index 189c5e0..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.notification.stack.ui.viewbinder
-
-import android.content.Context
-import android.util.TypedValue
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.statusbar.notification.stack.AmbientState
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
-import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
-import kotlin.math.roundToInt
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.DisposableHandle
-import kotlinx.coroutines.launch
-
-/** Binds the shared notification container to its view-model. */
-object NotificationStackAppearanceViewBinder {
-    const val SCRIM_CORNER_RADIUS = 32f
-
-    @JvmStatic
-    fun bind(
-        context: Context,
-        view: SharedNotificationContainer,
-        viewModel: NotificationStackAppearanceViewModel,
-        ambientState: AmbientState,
-        controller: NotificationStackScrollLayoutController,
-        @Main mainImmediateDispatcher: CoroutineDispatcher,
-    ): DisposableHandle {
-        return view.repeatWhenAttached(mainImmediateDispatcher) {
-            repeatOnLifecycle(Lifecycle.State.CREATED) {
-                launch {
-                    viewModel.stackClipping.collect { (bounds, rounding) ->
-                        val viewLeft = controller.view.left
-                        val viewTop = controller.view.top
-                        val roundRadius = SCRIM_CORNER_RADIUS.dpToPx(context)
-                        controller.setRoundedClippingBounds(
-                            bounds.left.roundToInt() - viewLeft,
-                            bounds.top.roundToInt() - viewTop,
-                            bounds.right.roundToInt() - viewLeft,
-                            bounds.bottom.roundToInt() - viewTop,
-                            if (rounding.roundTop) roundRadius else 0,
-                            if (rounding.roundBottom) roundRadius else 0,
-                        )
-                    }
-                }
-
-                launch {
-                    viewModel.contentTop.collect {
-                        controller.updateTopPadding(it, controller.isAddOrRemoveAnimationPending)
-                    }
-                }
-
-                launch {
-                    var wasExpanding = false
-                    viewModel.expandFraction.collect { expandFraction ->
-                        val nowExpanding = expandFraction != 0f && expandFraction != 1f
-                        if (nowExpanding && !wasExpanding) {
-                            controller.onExpansionStarted()
-                        }
-                        ambientState.expansionFraction = expandFraction
-                        controller.expandedHeight = expandFraction * controller.view.height
-                        if (!nowExpanding && wasExpanding) {
-                            controller.onExpansionStopped()
-                        }
-                        wasExpanding = nowExpanding
-                    }
-                }
-
-                launch { viewModel.isScrollable.collect { controller.setScrollingEnabled(it) } }
-            }
-        }
-    }
-
-    private fun Float.dpToPx(context: Context): Int {
-        return TypedValue.applyDimension(
-                TypedValue.COMPLEX_UNIT_DIP,
-                this,
-                context.resources.displayMetrics
-            )
-            .roundToInt()
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt
new file mode 100644
index 0000000..1a34bb4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.ui.viewbinder
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.common.ui.ConfigurationState
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.notification.stack.AmbientState
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
+import javax.inject.Inject
+import kotlin.math.roundToInt
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
+
+/** Binds the NSSL/Controller/AmbientState to their ViewModel. */
+@SysUISingleton
+class NotificationStackViewBinder
+@Inject
+constructor(
+    @Main private val mainImmediateDispatcher: CoroutineDispatcher,
+    private val ambientState: AmbientState,
+    private val view: NotificationStackScrollLayout,
+    private val controller: NotificationStackScrollLayoutController,
+    private val viewModel: NotificationStackAppearanceViewModel,
+    private val configuration: ConfigurationState,
+) {
+
+    fun bindWhileAttached(): DisposableHandle {
+        return view.repeatWhenAttached(mainImmediateDispatcher) {
+            repeatOnLifecycle(Lifecycle.State.CREATED) { bind() }
+        }
+    }
+
+    suspend fun bind() = coroutineScope {
+        launch {
+            combine(viewModel.stackClipping, clipRadius, ::Pair).collect { (clipping, clipRadius) ->
+                val (bounds, rounding) = clipping
+                val viewLeft = controller.view.left
+                val viewTop = controller.view.top
+                controller.setRoundedClippingBounds(
+                    bounds.left.roundToInt() - viewLeft,
+                    bounds.top.roundToInt() - viewTop,
+                    bounds.right.roundToInt() - viewLeft,
+                    bounds.bottom.roundToInt() - viewTop,
+                    if (rounding.roundTop) clipRadius else 0,
+                    if (rounding.roundBottom) clipRadius else 0,
+                )
+            }
+        }
+
+        launch {
+            viewModel.contentTop.collect {
+                controller.updateTopPadding(it, controller.isAddOrRemoveAnimationPending)
+            }
+        }
+
+        launch {
+            var wasExpanding = false
+            viewModel.expandFraction.collect { expandFraction ->
+                val nowExpanding = expandFraction != 0f && expandFraction != 1f
+                if (nowExpanding && !wasExpanding) {
+                    controller.onExpansionStarted()
+                }
+                ambientState.expansionFraction = expandFraction
+                controller.expandedHeight = expandFraction * controller.view.height
+                if (!nowExpanding && wasExpanding) {
+                    controller.onExpansionStopped()
+                }
+                wasExpanding = nowExpanding
+            }
+        }
+
+        launch { viewModel.isScrollable.collect { controller.setScrollingEnabled(it) } }
+    }
+
+    private val clipRadius: Flow<Int>
+        get() = configuration.getDimensionPixelOffset(R.dimen.notification_scrim_corner_radius)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
index 7c76ddb..6db6719 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
@@ -20,6 +20,7 @@
 import android.view.WindowInsets
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
 import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
@@ -30,6 +31,8 @@
 import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
+import com.android.systemui.util.kotlin.DisposableHandles
+import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.DisposableHandle
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -38,18 +41,23 @@
 import kotlinx.coroutines.launch
 
 /** Binds the shared notification container to its view-model. */
-object SharedNotificationContainerBinder {
+@SysUISingleton
+class SharedNotificationContainerBinder
+@Inject
+constructor(
+    private val sceneContainerFlags: SceneContainerFlags,
+    private val controller: NotificationStackScrollLayoutController,
+    private val notificationStackSizeCalculator: NotificationStackSizeCalculator,
+    @Main private val mainImmediateDispatcher: CoroutineDispatcher,
+) {
 
-    @JvmStatic
     fun bind(
         view: SharedNotificationContainer,
         viewModel: SharedNotificationContainerViewModel,
-        sceneContainerFlags: SceneContainerFlags,
-        controller: NotificationStackScrollLayoutController,
-        notificationStackSizeCalculator: NotificationStackSizeCalculator,
-        @Main mainImmediateDispatcher: CoroutineDispatcher,
     ): DisposableHandle {
-        val disposableHandle =
+        val disposables = DisposableHandles()
+
+        disposables +=
             view.repeatWhenAttached {
                 repeatOnLifecycle(Lifecycle.State.CREATED) {
                     launch {
@@ -72,24 +80,6 @@
                 }
             }
 
-        // Required to capture keyguard media changes and ensure the notification count is correct
-        val layoutChangeListener =
-            object : View.OnLayoutChangeListener {
-                override fun onLayoutChange(
-                    view: View,
-                    left: Int,
-                    top: Int,
-                    right: Int,
-                    bottom: Int,
-                    oldLeft: Int,
-                    oldTop: Int,
-                    oldRight: Int,
-                    oldBottom: Int
-                ) {
-                    viewModel.notificationStackChanged()
-                }
-            }
-
         val burnInParams = MutableStateFlow(BurnInParameters())
         val viewState =
             ViewStateAccessor(
@@ -100,7 +90,7 @@
          * For animation sensitive coroutines, immediately run just like applicationScope does
          * instead of doing a post() to the main thread. This extra delay can cause visible jitter.
          */
-        val disposableHandleMainImmediate =
+        disposables +=
             view.repeatWhenAttached(mainImmediateDispatcher) {
                 repeatOnLifecycle(Lifecycle.State.CREATED) {
                     launch {
@@ -167,7 +157,8 @@
                 }
             }
 
-        controller.setOnHeightChangedRunnable(Runnable { viewModel.notificationStackChanged() })
+        controller.setOnHeightChangedRunnable { viewModel.notificationStackChanged() }
+        disposables += DisposableHandle { controller.setOnHeightChangedRunnable(null) }
 
         view.setOnApplyWindowInsetsListener { v: View, insets: WindowInsets ->
             val insetTypes = WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout()
@@ -176,16 +167,16 @@
             }
             insets
         }
-        view.addOnLayoutChangeListener(layoutChangeListener)
+        disposables += DisposableHandle { view.setOnApplyWindowInsetsListener(null) }
 
-        return object : DisposableHandle {
-            override fun dispose() {
-                disposableHandle.dispose()
-                disposableHandleMainImmediate.dispose()
-                controller.setOnHeightChangedRunnable(null)
-                view.setOnApplyWindowInsetsListener(null)
-                view.removeOnLayoutChangeListener(layoutChangeListener)
+        // Required to capture keyguard media changes and ensure the notification count is correct
+        val layoutChangeListener =
+            View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
+                viewModel.notificationStackChanged()
             }
-        }
+        view.addOnLayoutChangeListener(layoutChangeListener)
+        disposables += DisposableHandle { view.removeOnLayoutChangeListener(layoutChangeListener) }
+
+        return disposables
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
index ed44f20..bd83121 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
@@ -28,7 +28,6 @@
 import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.StateFlow
 
 /**
  * ViewModel used by the Notification placeholders inside the scene container to update the
@@ -73,7 +72,7 @@
     }
 
     /** Corner rounding of the stack */
-    val stackRounding: StateFlow<StackRounding> = interactor.stackRounding
+    val stackRounding: Flow<StackRounding> = interactor.stackRounding
 
     /**
      * The height in px of the contents of notification stack. Depending on the number of
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandles.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandles.kt
new file mode 100644
index 0000000..de036ea
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandles.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.kotlin
+
+import kotlinx.coroutines.DisposableHandle
+
+/** A mutable collection of [DisposableHandle] objects that is itself a [DisposableHandle] */
+class DisposableHandles : DisposableHandle {
+    private val handles = mutableListOf<DisposableHandle>()
+
+    /** Add the provided handles to this collection. */
+    fun add(vararg handles: DisposableHandle) {
+        this.handles.addAll(handles)
+    }
+
+    /** Same as [add] */
+    operator fun plusAssign(handle: DisposableHandle) {
+        this.handles.add(handle)
+    }
+
+    /** Same as [add] */
+    operator fun plusAssign(handles: Iterable<DisposableHandle>) {
+        this.handles.addAll(handles)
+    }
+
+    /** [dispose] the current contents, then [add] the provided [handles] */
+    fun replaceAll(vararg handles: DisposableHandle) {
+        dispose()
+        add(*handles)
+    }
+
+    /** Dispose of all added handles and empty this collection. */
+    override fun dispose() {
+        handles.forEach { it.dispose() }
+        handles.clear()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt
index 5dd37ae..66aa572 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt
@@ -131,7 +131,6 @@
         whenever(clock.smallClock).thenReturn(smallClock)
         whenever(largeClock.layout).thenReturn(largeClockFaceLayout)
         whenever(smallClock.layout).thenReturn(smallClockFaceLayout)
-        whenever(clockViewModel.clock).thenReturn(clock)
         currentClock.value = clock
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
index 2e8160b..1cfca68 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
@@ -222,4 +222,9 @@
             )
         verify(factory, never()).create(any<ScreenCapturePermissionDialogDelegate>())
     }
+
+    @Test
+    fun startButton_isDisabled_beforeIssueTypeIsSelected() {
+        assertThat(dialog.getButton(Dialog.BUTTON_POSITIVE).isEnabled).isFalse()
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt
index 546a1e0..5605d10 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt
@@ -18,10 +18,12 @@
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.statusbar.notification.stack.data.repository.notificationStackAppearanceRepository
 
 val Kosmos.notificationStackAppearanceInteractor by Fixture {
     NotificationStackAppearanceInteractor(
         repository = notificationStackAppearanceRepository,
+        shadeInteractor = shadeInteractor,
     )
 }
diff --git a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
index 66abb42..b8ef03f 100644
--- a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
+++ b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
@@ -19,6 +19,7 @@
 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;
@@ -27,6 +28,7 @@
 import android.app.AppOpsManager;
 import android.app.BroadcastOptions;
 import android.app.PendingIntent;
+import android.content.AttributionSource;
 import android.content.ComponentName;
 import android.content.IIntentSender;
 import android.content.Intent;
@@ -39,6 +41,7 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.permission.IPermissionManager;
+import android.permission.PermissionManager;
 import android.util.Slog;
 
 import com.android.internal.util.ArrayUtils;
@@ -54,6 +57,9 @@
 public class BroadcastSkipPolicy {
     private final ActivityManagerService mService;
 
+    @Nullable
+    private PermissionManager mPermissionManager;
+
     public BroadcastSkipPolicy(@NonNull ActivityManagerService service) {
         mService = Objects.requireNonNull(service);
     }
@@ -283,14 +289,35 @@
 
         if (info.activityInfo.applicationInfo.uid != Process.SYSTEM_UID &&
                 r.requiredPermissions != null && r.requiredPermissions.length > 0) {
+            final AttributionSource attributionSource;
+            if (usePermissionManagerForBroadcastDeliveryCheck()) {
+                attributionSource =
+                        new AttributionSource.Builder(info.activityInfo.applicationInfo.uid)
+                                .setPackageName(info.activityInfo.packageName)
+                                .build();
+            } else {
+                attributionSource = null;
+            }
             for (int i = 0; i < r.requiredPermissions.length; i++) {
                 String requiredPermission = r.requiredPermissions[i];
                 try {
-                    perm = AppGlobals.getPackageManager().
-                            checkPermission(requiredPermission,
-                                    info.activityInfo.applicationInfo.packageName,
-                                    UserHandle
-                                    .getUserId(info.activityInfo.applicationInfo.uid));
+                    if (usePermissionManagerForBroadcastDeliveryCheck()) {
+                        final PermissionManager permissionManager = getPermissionManager();
+                        if (permissionManager != null) {
+                            perm = permissionManager.checkPermissionForDataDelivery(
+                                    requiredPermission, attributionSource, null /* message */);
+                        } else {
+                            // Assume permission denial if PermissionManager is not yet available.
+                            perm = 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;
                 }
@@ -302,11 +329,13 @@
                             + " due to sender " + r.callerPackage
                             + " (uid " + r.callingUid + ")";
                 }
-                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 (!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;
+                        }
                     }
                 }
             }
@@ -694,4 +723,11 @@
 
         return false;
     }
+
+    private PermissionManager getPermissionManager() {
+        if (mPermissionManager == null) {
+            mPermissionManager = mService.mContext.getSystemService(PermissionManager.class);
+        }
+        return mPermissionManager;
+    }
 }
diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig
index 0209944..fd847f1 100644
--- a/services/core/java/com/android/server/am/flags.aconfig
+++ b/services/core/java/com/android/server/am/flags.aconfig
@@ -86,3 +86,11 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    namespace: "backstage_power"
+    name: "use_permission_manager_for_broadcast_delivery_check"
+    description: "Use PermissionManager API for broadcast delivery permission checks."
+    bug: "315468967"
+    is_fixed_read_only: true
+}
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index e8c05c6..de000bf 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -73,6 +73,7 @@
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
 import android.bluetooth.BluetoothProfile;
+import android.content.AttributionSource;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.ContentResolver;
@@ -12207,7 +12208,9 @@
     //==========================================================================================
     public String registerAudioPolicy(AudioPolicyConfig policyConfig, IAudioPolicyCallback pcb,
             boolean hasFocusListener, boolean isFocusPolicy, boolean isTestFocusPolicy,
-            boolean isVolumeController, IMediaProjection projection) {
+            boolean isVolumeController, IMediaProjection projection,
+            AttributionSource attributionSource) {
+        Objects.requireNonNull(attributionSource);
         AudioSystem.setDynamicPolicyCallback(mDynPolicyCallback);
 
         if (!isPolicyRegisterAllowed(policyConfig,
@@ -12228,7 +12231,8 @@
             }
             try {
                 AudioPolicyProxy app = new AudioPolicyProxy(policyConfig, pcb, hasFocusListener,
-                        isFocusPolicy, isTestFocusPolicy, isVolumeController, projection);
+                        isFocusPolicy, isTestFocusPolicy, isVolumeController, projection,
+                        attributionSource);
                 pcb.asBinder().linkToDeath(app, 0/*flags*/);
 
                 // logging after registration so we have the registration id
@@ -13200,6 +13204,7 @@
     public class AudioPolicyProxy extends AudioPolicyConfig implements IBinder.DeathRecipient {
         private static final String TAG = "AudioPolicyProxy";
         final IAudioPolicyCallback mPolicyCallback;
+        final AttributionSource mAttributionSource;
         final boolean mHasFocusListener;
         final boolean mIsVolumeController;
         final HashMap<Integer, AudioDeviceArray> mUidDeviceAffinities =
@@ -13239,10 +13244,12 @@
 
         AudioPolicyProxy(AudioPolicyConfig config, IAudioPolicyCallback token,
                 boolean hasFocusListener, boolean isFocusPolicy, boolean isTestFocusPolicy,
-                boolean isVolumeController, IMediaProjection projection) {
+                boolean isVolumeController, IMediaProjection projection,
+                AttributionSource attributionSource) {
             super(config);
             setRegistration(new String(config.hashCode() + ":ap:" + mAudioPolicyCounter++));
             mPolicyCallback = token;
+            mAttributionSource = attributionSource;
             mHasFocusListener = hasFocusListener;
             mIsVolumeController = isVolumeController;
             mProjection = projection;
@@ -13370,6 +13377,7 @@
                 if (android.media.audiopolicy.Flags.audioMixOwnership()) {
                     for (AudioMix mix : mixes) {
                         setMixRegistration(mix);
+                        mix.setVirtualDeviceId(mAttributionSource.getDeviceId());
                     }
 
                     int result = mAudioSystem.registerPolicyMixes(mixes, true);
@@ -13393,6 +13401,9 @@
         @AudioSystem.AudioSystemError int connectMixes() {
             final long identity = Binder.clearCallingIdentity();
             try {
+                for (AudioMix mix : mMixes) {
+                    mix.setVirtualDeviceId(mAttributionSource.getDeviceId());
+                }
                 return mAudioSystem.registerPolicyMixes(mMixes, true);
             } finally {
                 Binder.restoreCallingIdentity(identity);
@@ -13406,6 +13417,9 @@
             Objects.requireNonNull(mixesToUpdate);
             Objects.requireNonNull(updatedMixingRules);
 
+            for (AudioMix mix : mixesToUpdate) {
+                mix.setVirtualDeviceId(mAttributionSource.getDeviceId());
+            }
             if (mixesToUpdate.length != updatedMixingRules.length) {
                 Log.e(TAG, "Provided list of audio mixes to update and corresponding mixing rules "
                         + "have mismatching length (mixesToUpdate.length = " + mixesToUpdate.length
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index d0a83a6..cfd64c4 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -1248,7 +1248,15 @@
             mService.publishLocalService();
             IInputMethodManager.Stub service;
             if (Flags.useZeroJankProxy()) {
-                service = new ZeroJankProxy(mService.mHandler::post, mService);
+                service =
+                        new ZeroJankProxy(
+                                mService.mHandler::post,
+                                mService,
+                                () -> {
+                                    synchronized (ImfLock.class) {
+                                        return mService.isInputShown();
+                                    }
+                                });
             } else {
                 service = mService;
             }
diff --git a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
index 396192e..136ab42c 100644
--- a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
+++ b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
@@ -46,7 +46,6 @@
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.ShellCallback;
-import android.util.ExceptionUtils;
 import android.util.Slog;
 import android.view.WindowManager;
 import android.view.inputmethod.CursorAnchorInfo;
@@ -77,6 +76,7 @@
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
+import java.util.function.BooleanSupplier;
 
 /**
  * A proxy that processes all {@link IInputMethodManager} calls asynchronously.
@@ -86,10 +86,12 @@
 
     private final IInputMethodManager mInner;
     private final Executor mExecutor;
+    private final BooleanSupplier mIsInputShown;
 
-    ZeroJankProxy(Executor executor, IInputMethodManager inner) {
+    ZeroJankProxy(Executor executor, IInputMethodManager inner, BooleanSupplier isInputShown) {
         mInner = inner;
         mExecutor = executor;
+        mIsInputShown = isInputShown;
     }
 
     private void offload(ThrowingRunnable r) {
@@ -163,8 +165,19 @@
             int lastClickTooType, ResultReceiver resultReceiver,
             @SoftInputShowHideReason int reason)
             throws RemoteException {
-        offload(() -> mInner.showSoftInput(client, windowToken, statsToken, flags, lastClickTooType,
-                resultReceiver, reason));
+        offload(
+                () -> {
+                    if (!mInner.showSoftInput(
+                            client,
+                            windowToken,
+                            statsToken,
+                            flags,
+                            lastClickTooType,
+                            resultReceiver,
+                            reason)) {
+                        sendResultReceiverFailure(resultReceiver);
+                    }
+                });
         return true;
     }
 
@@ -173,11 +186,24 @@
             @Nullable ImeTracker.Token statsToken, @InputMethodManager.HideFlags int flags,
             ResultReceiver resultReceiver, @SoftInputShowHideReason int reason)
             throws RemoteException {
-        offload(() -> mInner.hideSoftInput(client, windowToken, statsToken, flags, resultReceiver,
-                reason));
+        offload(
+                () -> {
+                    if (!mInner.hideSoftInput(
+                            client, windowToken, statsToken, flags, resultReceiver, reason)) {
+                        sendResultReceiverFailure(resultReceiver);
+                    }
+                });
         return true;
     }
 
+    private void sendResultReceiverFailure(ResultReceiver resultReceiver) {
+        resultReceiver.send(
+                mIsInputShown.getAsBoolean()
+                        ? InputMethodManager.RESULT_UNCHANGED_SHOWN
+                        : InputMethodManager.RESULT_UNCHANGED_HIDDEN,
+                null);
+    }
+
     @Override
     @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD)
     public void hideSoftInputFromServerForTest() throws RemoteException {
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index a9a8272..5b3934e 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -687,27 +687,20 @@
 
     private static String toVolumeControlTypeString(
             @VolumeProvider.ControlType int volumeControlType) {
-        switch (volumeControlType) {
-            case VOLUME_CONTROL_FIXED:
-                return "FIXED";
-            case VOLUME_CONTROL_RELATIVE:
-                return "RELATIVE";
-            case VOLUME_CONTROL_ABSOLUTE:
-                return "ABSOLUTE";
-            default:
-                return TextUtils.formatSimple("unknown(%d)", volumeControlType);
-        }
+        return switch (volumeControlType) {
+            case VOLUME_CONTROL_FIXED -> "FIXED";
+            case VOLUME_CONTROL_RELATIVE -> "RELATIVE";
+            case VOLUME_CONTROL_ABSOLUTE -> "ABSOLUTE";
+            default -> TextUtils.formatSimple("unknown(%d)", volumeControlType);
+        };
     }
 
     private static String toVolumeTypeString(@PlaybackInfo.PlaybackType int volumeType) {
-        switch (volumeType) {
-            case PLAYBACK_TYPE_LOCAL:
-                return "LOCAL";
-            case PLAYBACK_TYPE_REMOTE:
-                return "REMOTE";
-            default:
-                return TextUtils.formatSimple("unknown(%d)", volumeType);
-        }
+        return switch (volumeType) {
+            case PLAYBACK_TYPE_LOCAL -> "LOCAL";
+            case PLAYBACK_TYPE_REMOTE -> "REMOTE";
+            default -> TextUtils.formatSimple("unknown(%d)", volumeType);
+        };
     }
 
     @Override
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 4f3cdbc..50ca984 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -310,6 +310,7 @@
                     parser.getAttributeInt(null, ATT_VISIBILITY, DEFAULT_VISIBILITY),
                     parser.getAttributeBoolean(null, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE),
                     bubblePref);
+            r.bubblePreference = bubblePref;
             r.priority = parser.getAttributeInt(null, ATT_PRIORITY, DEFAULT_PRIORITY);
             r.visibility = parser.getAttributeInt(null, ATT_VISIBILITY, DEFAULT_VISIBILITY);
             r.showBadge = parser.getAttributeBoolean(null, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE);
@@ -676,7 +677,7 @@
      * @param bubblePreference whether bubbles are allowed.
      */
     public void setBubblesAllowed(String pkg, int uid, int bubblePreference) {
-        boolean changed = false;
+        boolean changed;
         synchronized (mPackagePreferences) {
             PackagePreferences p = getOrCreatePackagePreferencesLocked(pkg, uid);
             changed = p.bubblePreference != bubblePreference;
diff --git a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
index 37023e1..953300a 100644
--- a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
+++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
@@ -163,7 +163,7 @@
             }
 
             @Override
-            public void getVersion(RemoteCallback remoteCallback) throws RemoteException {
+            public void getVersion(RemoteCallback remoteCallback) {
                 Slog.i(TAG, "OnDeviceIntelligenceManagerInternal getVersion");
                 Objects.requireNonNull(remoteCallback);
                 mContext.enforceCallingOrSelfPermission(
@@ -244,7 +244,7 @@
 
             @Override
             public void requestFeatureDownload(Feature feature,
-                    ICancellationSignal cancellationSignal,
+                    AndroidFuture cancellationSignalFuture,
                     IDownloadCallback downloadCallback) throws RemoteException {
                 Slog.i(TAG, "OnDeviceIntelligenceManagerInternal requestFeatureDownload");
                 Objects.requireNonNull(feature);
@@ -261,16 +261,17 @@
                 ensureRemoteIntelligenceServiceInitialized();
                 mRemoteOnDeviceIntelligenceService.run(
                         service -> service.requestFeatureDownload(Binder.getCallingUid(), feature,
-                                cancellationSignal,
+                                cancellationSignalFuture,
                                 downloadCallback));
             }
 
 
             @Override
             public void requestTokenInfo(Feature feature,
-                    Bundle request, ICancellationSignal cancellationSignal,
+                    Bundle request,
+                    AndroidFuture cancellationSignalFuture,
                     ITokenInfoCallback tokenInfoCallback) throws RemoteException {
-                Slog.i(TAG, "OnDeviceIntelligenceManagerInternal prepareFeatureProcessing");
+                Slog.i(TAG, "OnDeviceIntelligenceManagerInternal requestTokenInfo");
                 Objects.requireNonNull(feature);
                 Objects.requireNonNull(request);
                 Objects.requireNonNull(tokenInfoCallback);
@@ -285,10 +286,11 @@
                             PersistableBundle.EMPTY);
                 }
                 ensureRemoteInferenceServiceInitialized();
+
                 mRemoteInferenceService.run(
                         service -> service.requestTokenInfo(Binder.getCallingUid(), feature,
                                 request,
-                                cancellationSignal,
+                                cancellationSignalFuture,
                                 tokenInfoCallback));
             }
 
@@ -296,8 +298,8 @@
             public void processRequest(Feature feature,
                     Bundle request,
                     int requestType,
-                    ICancellationSignal cancellationSignal,
-                    IProcessingSignal processingSignal,
+                    AndroidFuture cancellationSignalFuture,
+                    AndroidFuture processingSignalFuture,
                     IResponseCallback responseCallback)
                     throws RemoteException {
                 Slog.i(TAG, "OnDeviceIntelligenceManagerInternal processRequest");
@@ -316,7 +318,7 @@
                 mRemoteInferenceService.run(
                         service -> service.processRequest(Binder.getCallingUid(), feature, request,
                                 requestType,
-                                cancellationSignal, processingSignal,
+                                cancellationSignalFuture, processingSignalFuture,
                                 responseCallback));
             }
 
@@ -324,8 +326,8 @@
             public void processRequestStreaming(Feature feature,
                     Bundle request,
                     int requestType,
-                    ICancellationSignal cancellationSignal,
-                    IProcessingSignal processingSignal,
+                    AndroidFuture cancellationSignalFuture,
+                    AndroidFuture processingSignalFuture,
                     IStreamingResponseCallback streamingCallback) throws RemoteException {
                 Slog.i(TAG, "OnDeviceIntelligenceManagerInternal processRequestStreaming");
                 Objects.requireNonNull(feature);
@@ -343,7 +345,7 @@
                 mRemoteInferenceService.run(
                         service -> service.processRequestStreaming(Binder.getCallingUid(), feature,
                                 request, requestType,
-                                cancellationSignal, processingSignal,
+                                cancellationSignalFuture, processingSignalFuture,
                                 streamingCallback));
             }
 
@@ -356,7 +358,7 @@
         };
     }
 
-    private void ensureRemoteIntelligenceServiceInitialized() throws RemoteException {
+    private void ensureRemoteIntelligenceServiceInitialized() {
         synchronized (mLock) {
             if (mRemoteOnDeviceIntelligenceService == null) {
                 String serviceName = getServiceNames()[0];
@@ -388,25 +390,15 @@
             public void updateProcessingState(
                     Bundle processingState,
                     IProcessingUpdateStatusCallback callback) {
-                try {
-                    ensureRemoteInferenceServiceInitialized();
-                    mRemoteInferenceService.run(
-                            service -> service.updateProcessingState(
-                                    processingState, callback));
-                } catch (RemoteException unused) {
-                    try {
-                        callback.onFailure(
-                                OnDeviceIntelligenceException.PROCESSING_UPDATE_STATUS_CONNECTION_FAILED,
-                                "Received failure invoking the remote processing service.");
-                    } catch (RemoteException ex) {
-                        Slog.w(TAG, "Failed to send failure status.", ex);
-                    }
-                }
+                ensureRemoteInferenceServiceInitialized();
+                mRemoteInferenceService.run(
+                        service -> service.updateProcessingState(
+                                processingState, callback));
             }
         };
     }
 
-    private void ensureRemoteInferenceServiceInitialized() throws RemoteException {
+    private void ensureRemoteInferenceServiceInitialized() {
         synchronized (mLock) {
             if (mRemoteInferenceService == null) {
                 String serviceName = getServiceNames()[1];
@@ -457,34 +449,38 @@
         };
     }
 
-    private static void validateServiceElevated(String serviceName, boolean checkIsolated)
-            throws RemoteException {
-        if (TextUtils.isEmpty(serviceName)) {
-            throw new IllegalArgumentException("Received null/empty service name : " + serviceName);
-        }
-        ComponentName serviceComponent = ComponentName.unflattenFromString(
-                serviceName);
-        ServiceInfo serviceInfo = AppGlobals.getPackageManager().getServiceInfo(
-                serviceComponent,
-                PackageManager.MATCH_DIRECT_BOOT_AWARE
-                        | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, 0);
-        if (serviceInfo != null) {
-            if (!checkIsolated) {
-                checkServiceRequiresPermission(serviceInfo,
-                        Manifest.permission.BIND_ON_DEVICE_INTELLIGENCE_SERVICE);
-                return;
+    private void validateServiceElevated(String serviceName, boolean checkIsolated) {
+        try {
+            if (TextUtils.isEmpty(serviceName)) {
+                throw new IllegalStateException(
+                        "Remote service is not configured to complete the request");
             }
+            ComponentName serviceComponent = ComponentName.unflattenFromString(
+                    serviceName);
+            ServiceInfo serviceInfo = AppGlobals.getPackageManager().getServiceInfo(
+                    serviceComponent,
+                    PackageManager.MATCH_DIRECT_BOOT_AWARE
+                            | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, 0);
+            if (serviceInfo != null) {
+                if (!checkIsolated) {
+                    checkServiceRequiresPermission(serviceInfo,
+                            Manifest.permission.BIND_ON_DEVICE_INTELLIGENCE_SERVICE);
+                    return;
+                }
 
-            checkServiceRequiresPermission(serviceInfo,
-                    Manifest.permission.BIND_ON_DEVICE_SANDBOXED_INFERENCE_SERVICE);
-            if (!isIsolatedService(serviceInfo)) {
-                throw new SecurityException(
-                        "Call required an isolated service, but the configured service: "
-                                + serviceName + ", is not isolated");
+                checkServiceRequiresPermission(serviceInfo,
+                        Manifest.permission.BIND_ON_DEVICE_SANDBOXED_INFERENCE_SERVICE);
+                if (!isIsolatedService(serviceInfo)) {
+                    throw new SecurityException(
+                            "Call required an isolated service, but the configured service: "
+                                    + serviceName + ", is not isolated");
+                }
+            } else {
+                throw new IllegalStateException(
+                        "Remote service is not configured to complete the request.");
             }
-        } else {
-            throw new RuntimeException(
-                    "Could not find service info for serviceName: " + serviceName);
+        } catch (RemoteException e) {
+            throw new IllegalStateException("Could not fetch service info for remote services", e);
         }
     }
 
@@ -542,7 +538,8 @@
                 Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG);
         synchronized (mLock) {
             mTemporaryServiceNames = componentNames;
-
+            mRemoteOnDeviceIntelligenceService = null;
+            mRemoteInferenceService = null;
             if (mTemporaryHandler == null) {
                 mTemporaryHandler = new Handler(Looper.getMainLooper(), null, true) {
                     @Override
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index bfc47fd..cee6cdb 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -3962,6 +3962,20 @@
     }
 
     @Test
+    public void testReadXml_existingPackage_bubblePrefsRestored() throws Exception {
+        mHelper.setBubblesAllowed(PKG_O, UID_O, BUBBLE_PREFERENCE_ALL);
+        assertEquals(BUBBLE_PREFERENCE_ALL, mHelper.getBubblePreference(PKG_O, UID_O));
+
+        mXmlHelper.setBubblesAllowed(PKG_O, UID_O, BUBBLE_PREFERENCE_NONE);
+        assertEquals(BUBBLE_PREFERENCE_NONE, mXmlHelper.getBubblePreference(PKG_O, UID_O));
+
+        ByteArrayOutputStream stream = writeXmlAndPurge(PKG_O, UID_O, false, UserHandle.USER_ALL);
+        loadStreamXml(stream, true, UserHandle.USER_ALL);
+
+        assertEquals(BUBBLE_PREFERENCE_ALL, mXmlHelper.getBubblePreference(PKG_O, UID_O));
+    }
+
+    @Test
     public void testUpdateNotificationChannel_fixedPermission() {
         List<UserInfo> users = ImmutableList.of(new UserInfo(UserHandle.USER_SYSTEM, "user0", 0));
         when(mPermissionHelper.isPermissionFixed(PKG_O, 0)).thenReturn(true);
diff --git a/tools/app_metadata_bundles/Android.bp b/tools/app_metadata_bundles/Android.bp
new file mode 100644
index 0000000..be6bea6
--- /dev/null
+++ b/tools/app_metadata_bundles/Android.bp
@@ -0,0 +1,26 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_library_host {
+    name: "asllib",
+    srcs: [
+        "src/lib/java/**/*.java",
+    ],
+}
+
+java_binary_host {
+    name: "aslgen",
+    manifest: "src/aslgen/aslgen.mf",
+    srcs: [
+        "src/aslgen/java/**/*.java",
+    ],
+    static_libs: [
+        "asllib",
+    ],
+}
diff --git a/tools/app_metadata_bundles/OWNERS b/tools/app_metadata_bundles/OWNERS
new file mode 100644
index 0000000..a2a250b
--- /dev/null
+++ b/tools/app_metadata_bundles/OWNERS
@@ -0,0 +1,2 @@
+wenhaowang@google.com
+mloh@google.com
diff --git a/tools/app_metadata_bundles/README.md b/tools/app_metadata_bundles/README.md
new file mode 100644
index 0000000..6e8d287
--- /dev/null
+++ b/tools/app_metadata_bundles/README.md
@@ -0,0 +1,9 @@
+# App metadata bundles
+
+This project delivers a comprehensive toolchain solution for developers
+to efficiently manage app metadata bundles.
+
+The project consists of two subprojects:
+
+  * A pure Java library, and
+  * A pure Java command-line tool.
diff --git a/tools/app_metadata_bundles/src/aslgen/aslgen.mf b/tools/app_metadata_bundles/src/aslgen/aslgen.mf
new file mode 100644
index 0000000..fc656e2
--- /dev/null
+++ b/tools/app_metadata_bundles/src/aslgen/aslgen.mf
@@ -0,0 +1 @@
+Main-Class: com.android.aslgen.Main
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/aslgen/java/com/android/aslgen/Main.java b/tools/app_metadata_bundles/src/aslgen/java/com/android/aslgen/Main.java
new file mode 100644
index 0000000..df003b6
--- /dev/null
+++ b/tools/app_metadata_bundles/src/aslgen/java/com/android/aslgen/Main.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.aslgen;
+
+import com.android.asllib.AndroidSafetyLabel;
+import com.android.asllib.AndroidSafetyLabel.Format;
+
+import org.xml.sax.SAXException;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.TransformerException;
+
+public class Main {
+
+    /** Takes the options to make file conversion. */
+    public static void main(String[] args)
+            throws IOException, ParserConfigurationException, SAXException, TransformerException {
+
+        String inFile = null;
+        String outFile = null;
+        Format inFormat = Format.NULL;
+        Format outFormat = Format.NULL;
+
+
+        // Except for "--help", all arguments require a value currently.
+        // So just make sure we have an even number and
+        // then process them all two at a time.
+        if (args.length == 1 && "--help".equals(args[0])) {
+            showUsage();
+            return;
+        }
+        if (args.length % 2 != 0) {
+            throw new IllegalArgumentException("Argument is missing corresponding value");
+        }
+        for (int i = 0; i < args.length - 1; i += 2) {
+            final String arg = args[i].trim();
+            final String argValue = args[i + 1].trim();
+            if ("--in-path".equals(arg)) {
+                inFile = argValue;
+            } else if ("--out-path".equals(arg)) {
+                outFile = argValue;
+            } else if ("--in-format".equals(arg)) {
+                inFormat = getFormat(argValue);
+            } else if ("--out-format".equals(arg)) {
+                outFormat = getFormat(argValue);
+            } else {
+                throw new IllegalArgumentException("Unknown argument: " + arg);
+            }
+        }
+
+        if (inFile == null) {
+            throw new IllegalArgumentException("input file is required");
+        }
+
+        if (outFile == null) {
+            throw new IllegalArgumentException("output file is required");
+        }
+
+        if (inFormat == Format.NULL) {
+            throw new IllegalArgumentException("input format is required");
+        }
+
+        if (outFormat == Format.NULL) {
+            throw new IllegalArgumentException("output format is required");
+        }
+
+        System.out.println("in path: " + inFile);
+        System.out.println("out path: " + outFile);
+        System.out.println("in format: " + inFormat);
+        System.out.println("out format: " + outFormat);
+
+        var asl = AndroidSafetyLabel.readFromStream(new FileInputStream(inFile), inFormat);
+        asl.writeToStream(new FileOutputStream(outFile), outFormat);
+    }
+
+    private static Format getFormat(String argValue) {
+        if ("hr".equals(argValue)) {
+            return Format.HUMAN_READABLE;
+        } else if ("od".equals(argValue)) {
+            return Format.ON_DEVICE;
+        } else {
+            return Format.NULL;
+        }
+    }
+
+    private static void showUsage() {
+        AndroidSafetyLabel.test();
+        System.err.println(
+                "Usage:\n"
+        );
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java
new file mode 100644
index 0000000..07e0e73
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java
@@ -0,0 +1,102 @@
+/*
+ * 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.asllib;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+public class AndroidSafetyLabel {
+
+    public enum Format {
+        NULL, HUMAN_READABLE, ON_DEVICE;
+    }
+
+    private final SafetyLabels mSafetyLabels;
+
+    public SafetyLabels getSafetyLabels() {
+        return mSafetyLabels;
+    }
+
+    private AndroidSafetyLabel(SafetyLabels safetyLabels) {
+        this.mSafetyLabels = safetyLabels;
+    }
+
+    /** Reads a {@link AndroidSafetyLabel} from an {@link InputStream}. */
+    // TODO(b/329902686): Support conversion in both directions, specified by format.
+    public static AndroidSafetyLabel readFromStream(InputStream in, Format format)
+            throws IOException, ParserConfigurationException, SAXException {
+        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        factory.setNamespaceAware(true);
+        Document document = factory.newDocumentBuilder().parse(in);
+
+        Element appMetadataBundles =
+                XmlUtils.getSingleElement(document, XmlUtils.HR_TAG_APP_METADATA_BUNDLES);
+
+        return AndroidSafetyLabel.createFromHrElement(appMetadataBundles);
+    }
+
+    /** Write the content of the {@link AndroidSafetyLabel} to a {@link OutputStream}. */
+    // TODO(b/329902686): Support conversion in both directions, specified by format.
+    public void writeToStream(OutputStream out, Format format)
+            throws IOException, ParserConfigurationException, TransformerException {
+        var docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+        var document = docBuilder.newDocument();
+        document.appendChild(this.toOdDomElement(document));
+
+        TransformerFactory transformerFactory = TransformerFactory.newInstance();
+        Transformer transformer = transformerFactory.newTransformer();
+        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
+        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
+        StreamResult streamResult = new StreamResult(out); // out
+        DOMSource domSource = new DOMSource(document);
+        transformer.transform(domSource, streamResult);
+    }
+
+    /** Creates an {@link AndroidSafetyLabel} from human-readable DOM element */
+    public static AndroidSafetyLabel createFromHrElement(Element appMetadataBundlesEle) {
+        Element safetyLabelsEle =
+                XmlUtils.getSingleElement(appMetadataBundlesEle, XmlUtils.HR_TAG_SAFETY_LABELS);
+        SafetyLabels safetyLabels = SafetyLabels.createFromHrElement(safetyLabelsEle);
+        return new AndroidSafetyLabel(safetyLabels);
+    }
+
+    /** Creates an on-device DOM element from an {@link AndroidSafetyLabel} */
+    public Element toOdDomElement(Document doc) {
+        Element aslEle = doc.createElement(XmlUtils.OD_TAG_BUNDLE);
+        aslEle.appendChild(mSafetyLabels.toOdDomElement(doc));
+        return aslEle;
+    }
+
+    public static void test() {
+        // TODO(b/329902686): Add tests.
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java
new file mode 100644
index 0000000..efdaa40
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib;
+
+import java.util.Map;
+
+/**
+ * Data usage category representation containing one or more {@link DataType}. Valid category keys
+ * are defined in {@link DataCategoryConstants}, each category has a valid set of types {@link
+ * DataType}, which are mapped in {@link DataTypeConstants}
+ */
+public class DataCategory {
+    private final Map<String, DataType> mDataTypes;
+
+    private DataCategory(Map<String, DataType> dataTypes) {
+        this.mDataTypes = dataTypes;
+    }
+
+    /** Return the type {@link Map} of String type key to {@link DataType} */
+
+    public Map<String, DataType> getDataTypes() {
+        return mDataTypes;
+    }
+
+    /** Creates a {@link DataCategory} given map of {@param dataTypes}. */
+    public static DataCategory create(Map<String, DataType> dataTypes) {
+        return new DataCategory(dataTypes);
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryConstants.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryConstants.java
new file mode 100644
index 0000000..b364c8b
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryConstants.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib;
+
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Constants for determining valid {@link String} data types for usage within {@link SafetyLabels},
+ * {@link DataCategory}, and {@link DataType}
+ */
+public class DataCategoryConstants {
+
+    public static final String CATEGORY_PERSONAL = "personal";
+    public static final String CATEGORY_FINANCIAL = "financial";
+    public static final String CATEGORY_LOCATION = "location";
+    public static final String CATEGORY_EMAIL_TEXT_MESSAGE = "email_text_message";
+    public static final String CATEGORY_PHOTO_VIDEO = "photo_video";
+    public static final String CATEGORY_AUDIO = "audio";
+    public static final String CATEGORY_STORAGE = "storage";
+    public static final String CATEGORY_HEALTH_FITNESS = "health_fitness";
+    public static final String CATEGORY_CONTACTS = "contacts";
+    public static final String CATEGORY_CALENDAR = "calendar";
+    public static final String CATEGORY_IDENTIFIERS = "identifiers";
+    public static final String CATEGORY_APP_PERFORMANCE = "app_performance";
+    public static final String CATEGORY_ACTIONS_IN_APP = "actions_in_app";
+    public static final String CATEGORY_SEARCH_AND_BROWSING = "search_and_browsing";
+
+    /** Set of valid categories */
+    public static final Set<String> VALID_CATEGORIES =
+            Collections.unmodifiableSet(
+                    new HashSet<>(
+                            Arrays.asList(
+                                    CATEGORY_PERSONAL,
+                                    CATEGORY_FINANCIAL,
+                                    CATEGORY_LOCATION,
+                                    CATEGORY_EMAIL_TEXT_MESSAGE,
+                                    CATEGORY_PHOTO_VIDEO,
+                                    CATEGORY_AUDIO,
+                                    CATEGORY_STORAGE,
+                                    CATEGORY_HEALTH_FITNESS,
+                                    CATEGORY_CONTACTS,
+                                    CATEGORY_CALENDAR,
+                                    CATEGORY_IDENTIFIERS,
+                                    CATEGORY_APP_PERFORMANCE,
+                                    CATEGORY_ACTIONS_IN_APP,
+                                    CATEGORY_SEARCH_AND_BROWSING)));
+
+    /** Returns {@link Set} of valid {@link String} category keys */
+    public static Set<String> getValidDataCategories() {
+        return VALID_CATEGORIES;
+    }
+
+    private DataCategoryConstants() {
+        /* do nothing - hide constructor */
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java
new file mode 100644
index 0000000..d2c3d75b
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java
@@ -0,0 +1,176 @@
+/*
+ * 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.asllib;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Data label representation with data shared and data collected maps containing zero or more {@link
+ * DataCategory}
+ */
+public class DataLabels {
+    private final Map<String, DataCategory> mDataAccessed;
+    private final Map<String, DataCategory> mDataCollected;
+    private final Map<String, DataCategory> mDataShared;
+
+    public DataLabels(
+            Map<String, DataCategory> dataAccessed,
+            Map<String, DataCategory> dataCollected,
+            Map<String, DataCategory> dataShared) {
+        mDataAccessed = dataAccessed;
+        mDataCollected = dataCollected;
+        mDataShared = dataShared;
+    }
+
+    /**
+     * Returns the data accessed {@link Map} of {@link com.android.asllib.DataCategoryConstants} to
+     * {@link DataCategory}
+     */
+    public Map<String, DataCategory> getDataAccessed() {
+        return mDataAccessed;
+    }
+
+    /**
+     * Returns the data collected {@link Map} of {@link com.android.asllib.DataCategoryConstants} to
+     * {@link DataCategory}
+     */
+    public Map<String, DataCategory> getDataCollected() {
+        return mDataCollected;
+    }
+
+    /**
+     * Returns the data shared {@link Map} of {@link com.android.asllib.DataCategoryConstants} to
+     * {@link DataCategory}
+     */
+    public Map<String, DataCategory> getDataShared() {
+        return mDataShared;
+    }
+
+    /** Creates a {@link DataLabels} from the human-readable DOM element. */
+    public static DataLabels createFromHrElement(Element ele) {
+        Map<String, DataCategory> dataAccessed =
+                getDataCategoriesWithTag(ele, XmlUtils.HR_TAG_DATA_ACCESSED);
+        Map<String, DataCategory> dataCollected =
+                getDataCategoriesWithTag(ele, XmlUtils.HR_TAG_DATA_COLLECTED);
+        Map<String, DataCategory> dataShared =
+                getDataCategoriesWithTag(ele, XmlUtils.HR_TAG_DATA_SHARED);
+        return new DataLabels(dataAccessed, dataCollected, dataShared);
+    }
+
+    private static Map<String, DataCategory> getDataCategoriesWithTag(
+            Element dataLabelsEle, String dataCategoryUsageTypeTag) {
+        Map<String, Map<String, DataType>> dataTypeMap =
+                new HashMap<String, Map<String, DataType>>();
+        NodeList dataSharedNodeList = dataLabelsEle.getElementsByTagName(dataCategoryUsageTypeTag);
+
+        for (int i = 0; i < dataSharedNodeList.getLength(); i++) {
+            Element dataSharedEle = (Element) dataSharedNodeList.item(i);
+            String dataCategoryName = dataSharedEle.getAttribute(XmlUtils.HR_ATTR_DATA_CATEGORY);
+            String dataTypeName = dataSharedEle.getAttribute(XmlUtils.HR_ATTR_DATA_TYPE);
+
+            if (!dataTypeMap.containsKey((dataCategoryName))) {
+                dataTypeMap.put(dataCategoryName, new HashMap<String, DataType>());
+            }
+            dataTypeMap
+                    .get(dataCategoryName)
+                    .put(dataTypeName, DataType.createFromHrElement(dataSharedEle));
+        }
+
+        Map<String, DataCategory> dataCategoryMap = new HashMap<String, DataCategory>();
+        for (String dataCategoryName : dataTypeMap.keySet()) {
+            Map<String, DataType> dataTypes = dataTypeMap.get(dataCategoryName);
+            dataCategoryMap.put(dataCategoryName, DataCategory.create(dataTypes));
+        }
+        return dataCategoryMap;
+    }
+
+    /** Gets the on-device DOM element for the {@link DataLabels}. */
+    public Element toOdDomElement(Document doc) {
+        Element dataLabelsEle =
+                XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_DATA_LABELS);
+
+        maybeAppendDataUsages(doc, dataLabelsEle, mDataCollected, XmlUtils.OD_NAME_DATA_ACCESSED);
+        maybeAppendDataUsages(doc, dataLabelsEle, mDataCollected, XmlUtils.OD_NAME_DATA_COLLECTED);
+        maybeAppendDataUsages(doc, dataLabelsEle, mDataShared, XmlUtils.OD_NAME_DATA_SHARED);
+
+        return dataLabelsEle;
+    }
+
+    private void maybeAppendDataUsages(
+            Document doc,
+            Element dataLabelsEle,
+            Map<String, DataCategory> dataCategoriesMap,
+            String dataUsageTypeName) {
+        if (dataCategoriesMap.isEmpty()) {
+            return;
+        }
+        Element dataUsageEle = XmlUtils.createPbundleEleWithName(doc, dataUsageTypeName);
+
+        for (String dataCategoryName : dataCategoriesMap.keySet()) {
+            Element dataCategoryEle = XmlUtils.createPbundleEleWithName(doc, dataCategoryName);
+            DataCategory dataCategory = dataCategoriesMap.get(dataCategoryName);
+            for (String dataTypeName : dataCategory.getDataTypes().keySet()) {
+                DataType dataType = dataCategory.getDataTypes().get(dataTypeName);
+                Element dataTypeEle = XmlUtils.createPbundleEleWithName(doc, dataTypeName);
+                if (!dataType.getPurposeSet().isEmpty()) {
+                    Element purposesEle = doc.createElement(XmlUtils.OD_TAG_INT_ARRAY);
+                    purposesEle.setAttribute(XmlUtils.OD_ATTR_NAME, XmlUtils.OD_NAME_PURPOSES);
+                    purposesEle.setAttribute(
+                            XmlUtils.OD_ATTR_NUM, String.valueOf(dataType.getPurposeSet().size()));
+                    for (DataType.Purpose purpose : dataType.getPurposeSet()) {
+                        Element purposeEle = doc.createElement(XmlUtils.OD_TAG_ITEM);
+                        purposeEle.setAttribute(
+                                XmlUtils.OD_ATTR_VALUE, String.valueOf(purpose.getValue()));
+                        purposesEle.appendChild(purposeEle);
+                    }
+                    dataTypeEle.appendChild(purposesEle);
+                }
+
+                maybeAddBoolToOdElement(
+                        doc,
+                        dataTypeEle,
+                        dataType.getIsCollectionOptional(),
+                        XmlUtils.OD_NAME_IS_COLLECTION_OPTIONAL);
+                maybeAddBoolToOdElement(
+                        doc,
+                        dataTypeEle,
+                        dataType.getIsSharingOptional(),
+                        XmlUtils.OD_NAME_IS_SHARING_OPTIONAL);
+                maybeAddBoolToOdElement(
+                        doc, dataTypeEle, dataType.getEphemeral(), XmlUtils.OD_NAME_EPHEMERAL);
+
+                dataCategoryEle.appendChild(dataTypeEle);
+            }
+            dataUsageEle.appendChild(dataCategoryEle);
+        }
+        dataLabelsEle.appendChild(dataUsageEle);
+    }
+
+    private static void maybeAddBoolToOdElement(
+            Document doc, Element parentEle, Boolean b, String odName) {
+        if (b == null) {
+            return;
+        }
+        Element ele = XmlUtils.createOdBooleanEle(doc, odName, b);
+        parentEle.appendChild(ele);
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java
new file mode 100644
index 0000000..7451c69
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java
@@ -0,0 +1,145 @@
+/*
+ * 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.asllib;
+
+import org.w3c.dom.Element;
+
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Data usage type representation. Types are specific to a {@link DataCategory} and contains
+ * metadata related to the data usage purpose.
+ */
+public class DataType {
+    public enum Purpose {
+        PURPOSE_APP_FUNCTIONALITY(1),
+        PURPOSE_ANALYTICS(2),
+        PURPOSE_DEVELOPER_COMMUNICATIONS(3),
+        PURPOSE_FRAUD_PREVENTION_SECURITY(4),
+        PURPOSE_ADVERTISING(5),
+        PURPOSE_PERSONALIZATION(6),
+        PURPOSE_ACCOUNT_MANAGEMENT(7);
+
+        private static final String PURPOSE_PREFIX = "PURPOSE_";
+
+        private final int mValue;
+
+        Purpose(int value) {
+            this.mValue = value;
+        }
+
+        /** Get the int value associated with the Purpose. */
+        public int getValue() {
+            return mValue;
+        }
+
+        /** Get the Purpose associated with the int value. */
+        public static Purpose forValue(int value) {
+            for (Purpose e : values()) {
+                if (e.getValue() == value) {
+                    return e;
+                }
+            }
+            throw new IllegalArgumentException("No enum for value: " + value);
+        }
+
+        /** Get the Purpose associated with the human-readable String. */
+        public static Purpose forString(String s) {
+            for (Purpose e : values()) {
+                if (e.toString().equals(s)) {
+                    return e;
+                }
+            }
+            throw new IllegalArgumentException("No enum for str: " + s);
+        }
+
+        /** Human-readable String representation of Purpose. */
+        public String toString() {
+            if (!this.name().startsWith(PURPOSE_PREFIX)) {
+                return this.name();
+            }
+            return this.name().substring(PURPOSE_PREFIX.length()).toLowerCase();
+        }
+    }
+
+    private final Set<Purpose> mPurposeSet;
+    private final Boolean mIsCollectionOptional;
+    private final Boolean mIsSharingOptional;
+    private final Boolean mEphemeral;
+
+    private DataType(
+            Set<Purpose> purposeSet,
+            Boolean isCollectionOptional,
+            Boolean isSharingOptional,
+            Boolean ephemeral) {
+        this.mPurposeSet = purposeSet;
+        this.mIsCollectionOptional = isCollectionOptional;
+        this.mIsSharingOptional = isSharingOptional;
+        this.mEphemeral = ephemeral;
+    }
+
+    /**
+     * Returns {@link Set} of valid {@link Integer} purposes for using the associated data category
+     * and type
+     */
+    public Set<Purpose> getPurposeSet() {
+        return mPurposeSet;
+    }
+
+    /**
+     * For data-collected, returns {@code true} if data usage is user optional and {@code false} if
+     * data usage is required. Should return {@code null} for data-accessed and data-shared.
+     */
+    public Boolean getIsCollectionOptional() {
+        return mIsCollectionOptional;
+    }
+
+    /**
+     * For data-shared, returns {@code true} if data usage is user optional and {@code false} if
+     * data usage is required. Should return {@code null} for data-accessed and data-collected.
+     */
+    public Boolean getIsSharingOptional() {
+        return mIsSharingOptional;
+    }
+
+    /**
+     * For data-collected, returns {@code true} if data usage is user optional and {@code false} if
+     * data usage is processed ephemerally. Should return {@code null} for data-shared.
+     */
+    public Boolean getEphemeral() {
+        return mEphemeral;
+    }
+
+    /** Creates a {@link DataType} from the human-readable DOM element. */
+    public static DataType createFromHrElement(Element hrDataTypeEle) {
+        Set<Purpose> purposeSet =
+                Arrays.stream(hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_PURPOSES).split("\\|"))
+                        .map(Purpose::forString)
+                        .collect(Collectors.toUnmodifiableSet());
+        Boolean isCollectionOptional =
+                XmlUtils.fromString(
+                        hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_IS_SHARING_OPTIONAL));
+        Boolean isSharingOptional =
+                XmlUtils.fromString(
+                        hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_IS_COLLECTION_OPTIONAL));
+        Boolean ephemeral =
+                XmlUtils.fromString(hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_EPHEMERAL));
+        return new DataType(purposeSet, isCollectionOptional, isSharingOptional, ephemeral);
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeConstants.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeConstants.java
new file mode 100644
index 0000000..a0a7537
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeConstants.java
@@ -0,0 +1,156 @@
+/*
+ * 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.asllib;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Constants for determining valid {@link String} data types for usage within {@link SafetyLabels},
+ * {@link DataCategory}, and {@link DataType}
+ */
+public class DataTypeConstants {
+    /** Data types for {@link DataCategoryConstants.CATEGORY_PERSONAL} */
+    public static final String TYPE_NAME = "name";
+
+    public static final String TYPE_EMAIL_ADDRESS = "email_address";
+    public static final String TYPE_PHONE_NUMBER = "phone_number";
+    public static final String TYPE_RACE_ETHNICITY = "race_ethnicity";
+    public static final String TYPE_POLITICAL_OR_RELIGIOUS_BELIEFS =
+            "political_or_religious_beliefs";
+    public static final String TYPE_SEXUAL_ORIENTATION_OR_GENDER_IDENTITY =
+            "sexual_orientation_or_gender_identity";
+    public static final String TYPE_PERSONAL_IDENTIFIERS = "personal_identifiers";
+    public static final String TYPE_OTHER = "other";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_FINANCIAL} */
+    public static final String TYPE_CARD_BANK_ACCOUNT = "card_bank_account";
+
+    public static final String TYPE_PURCHASE_HISTORY = "purchase_history";
+    public static final String TYPE_CREDIT_SCORE = "credit_score";
+    public static final String TYPE_FINANCIAL_OTHER = "other";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_LOCATION} */
+    public static final String TYPE_APPROX_LOCATION = "approx_location";
+
+    public static final String TYPE_PRECISE_LOCATION = "precise_location";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_EMAIL_TEXT_MESSAGE} */
+    public static final String TYPE_EMAILS = "emails";
+
+    public static final String TYPE_TEXT_MESSAGES = "text_messages";
+    public static final String TYPE_EMAIL_TEXT_MESSAGE_OTHER = "other";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_PHOTO_VIDEO} */
+    public static final String TYPE_PHOTOS = "photos";
+
+    public static final String TYPE_VIDEOS = "videos";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_AUDIO} */
+    public static final String TYPE_SOUND_RECORDINGS = "sound_recordings";
+
+    public static final String TYPE_MUSIC_FILES = "music_files";
+    public static final String TYPE_AUDIO_OTHER = "other";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_STORAGE} */
+    public static final String TYPE_FILES_DOCS = "files_docs";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_HEALTH_FITNESS} */
+    public static final String TYPE_HEALTH = "health";
+
+    public static final String TYPE_FITNESS = "fitness";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_CONTACTS} */
+    public static final String TYPE_CONTACTS = "contacts";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_CALENDAR} */
+    public static final String TYPE_CALENDAR = "calendar";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_IDENTIFIERS} */
+    public static final String TYPE_IDENTIFIERS_OTHER = "other";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_APP_PERFORMANCE} */
+    public static final String TYPE_CRASH_LOGS = "crash_logs";
+
+    public static final String TYPE_PERFORMANCE_DIAGNOSTICS = "performance_diagnostics";
+    public static final String TYPE_APP_PERFORMANCE_OTHER = "other";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_ACTIONS_IN_APP} */
+    public static final String TYPE_USER_INTERACTION = "user_interaction";
+
+    public static final String TYPE_IN_APP_SEARCH_HISTORY = "in_app_search_history";
+    public static final String TYPE_INSTALLED_APPS = "installed_apps";
+    public static final String TYPE_USER_GENERATED_CONTENT = "user_generated_content";
+    public static final String TYPE_ACTIONS_IN_APP_OTHER = "other";
+
+    /** Data types for {@link DataCategoryConstants.CATEGORY_SEARCH_AND_BROWSING} */
+    public static final String TYPE_WEB_BROWSING_HISTORY = "web_browsing_history";
+
+    /** Set of valid categories */
+    public static final Set<String> VALID_TYPES =
+            Collections.unmodifiableSet(
+                    new HashSet<>(
+                            Arrays.asList(
+                                    TYPE_NAME,
+                                    TYPE_EMAIL_ADDRESS,
+                                    TYPE_PHONE_NUMBER,
+                                    TYPE_RACE_ETHNICITY,
+                                    TYPE_POLITICAL_OR_RELIGIOUS_BELIEFS,
+                                    TYPE_SEXUAL_ORIENTATION_OR_GENDER_IDENTITY,
+                                    TYPE_PERSONAL_IDENTIFIERS,
+                                    TYPE_OTHER,
+                                    TYPE_CARD_BANK_ACCOUNT,
+                                    TYPE_PURCHASE_HISTORY,
+                                    TYPE_CREDIT_SCORE,
+                                    TYPE_FINANCIAL_OTHER,
+                                    TYPE_APPROX_LOCATION,
+                                    TYPE_PRECISE_LOCATION,
+                                    TYPE_EMAILS,
+                                    TYPE_TEXT_MESSAGES,
+                                    TYPE_EMAIL_TEXT_MESSAGE_OTHER,
+                                    TYPE_PHOTOS,
+                                    TYPE_VIDEOS,
+                                    TYPE_SOUND_RECORDINGS,
+                                    TYPE_MUSIC_FILES,
+                                    TYPE_AUDIO_OTHER,
+                                    TYPE_FILES_DOCS,
+                                    TYPE_HEALTH,
+                                    TYPE_FITNESS,
+                                    TYPE_CONTACTS,
+                                    TYPE_CALENDAR,
+                                    TYPE_IDENTIFIERS_OTHER,
+                                    TYPE_CRASH_LOGS,
+                                    TYPE_PERFORMANCE_DIAGNOSTICS,
+                                    TYPE_APP_PERFORMANCE_OTHER,
+                                    TYPE_USER_INTERACTION,
+                                    TYPE_IN_APP_SEARCH_HISTORY,
+                                    TYPE_INSTALLED_APPS,
+                                    TYPE_USER_GENERATED_CONTENT,
+                                    TYPE_ACTIONS_IN_APP_OTHER,
+                                    TYPE_WEB_BROWSING_HISTORY)));
+
+    /** Returns {@link Set} of valid {@link String} category keys */
+    public static Set<String> getValidDataTypes() {
+        return VALID_TYPES;
+    }
+
+    private DataTypeConstants() {
+        /* do nothing - hide constructor */
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java
new file mode 100644
index 0000000..6ba15e1
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java
@@ -0,0 +1,65 @@
+/*
+ * 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.asllib;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+/** Safety Label representation containing zero or more {@link DataCategory} for data shared */
+public class SafetyLabels {
+
+    private final Long mVersion;
+    private final DataLabels mDataLabels;
+
+    private SafetyLabels(Long version, DataLabels dataLabels) {
+        this.mVersion = version;
+        this.mDataLabels = dataLabels;
+    }
+
+    /** Returns the data label for the safety label */
+    public DataLabels getDataLabel() {
+        return mDataLabels;
+    }
+
+    /** Gets the version of the {@link SafetyLabels}. */
+    public Long getVersion() {
+        return mVersion;
+    }
+
+    /** Creates a {@link SafetyLabels} from the human-readable DOM element. */
+    public static SafetyLabels createFromHrElement(Element safetyLabelsEle) {
+        Long version;
+        try {
+            version = Long.parseLong(safetyLabelsEle.getAttribute(XmlUtils.HR_ATTR_VERSION));
+        } catch (Exception e) {
+            throw new IllegalArgumentException(
+                    "Malformed or missing required version in safety labels.");
+        }
+        Element dataLabelsEle =
+                XmlUtils.getSingleElement(safetyLabelsEle, XmlUtils.HR_TAG_DATA_LABELS);
+        DataLabels dataLabels = DataLabels.createFromHrElement(dataLabelsEle);
+        return new SafetyLabels(version, dataLabels);
+    }
+
+    /** Creates an on-device DOM element from the {@link SafetyLabels}. */
+    public Element toOdDomElement(Document doc) {
+        Element safetyLabelsEle =
+                XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_SAFETY_LABELS);
+        safetyLabelsEle.appendChild(mDataLabels.toOdDomElement(doc));
+        return safetyLabelsEle;
+    }
+}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java
new file mode 100644
index 0000000..4392c2c
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java
@@ -0,0 +1,119 @@
+/*
+ * 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.asllib;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+public class XmlUtils {
+    public static final String HR_TAG_APP_METADATA_BUNDLES = "app-metadata-bundles";
+    public static final String HR_TAG_SAFETY_LABELS = "safety-labels";
+    public static final String HR_TAG_DATA_LABELS = "data-labels";
+    public static final String HR_TAG_DATA_ACCESSED = "data-accessed";
+    public static final String HR_TAG_DATA_COLLECTED = "data-collected";
+    public static final String HR_TAG_DATA_SHARED = "data-shared";
+
+    public static final String HR_ATTR_DATA_CATEGORY = "dataCategory";
+    public static final String HR_ATTR_DATA_TYPE = "dataType";
+    public static final String HR_ATTR_IS_COLLECTION_OPTIONAL = "isCollectionOptional";
+    public static final String HR_ATTR_IS_SHARING_OPTIONAL = "isSharingOptional";
+    public static final String HR_ATTR_EPHEMERAL = "ephemeral";
+    public static final String HR_ATTR_PURPOSES = "purposes";
+    public static final String HR_ATTR_VERSION = "version";
+
+    public static final String OD_TAG_BUNDLE = "bundle";
+    public static final String OD_TAG_PBUNDLE_AS_MAP = "pbundle_as_map";
+    public static final String OD_TAG_BOOLEAN = "boolean";
+    public static final String OD_TAG_INT_ARRAY = "int-array";
+    public static final String OD_TAG_ITEM = "item";
+    public static final String OD_ATTR_NAME = "name";
+    public static final String OD_ATTR_VALUE = "value";
+    public static final String OD_ATTR_NUM = "num";
+    public static final String OD_NAME_SAFETY_LABELS = "safety_labels";
+    public static final String OD_NAME_DATA_LABELS = "data_labels";
+    public static final String OD_NAME_DATA_ACCESSED = "data_accessed";
+    public static final String OD_NAME_DATA_COLLECTED = "data_collected";
+    public static final String OD_NAME_DATA_SHARED = "data_shared";
+    public static final String OD_NAME_PURPOSES = "purposes";
+    public static final String OD_NAME_IS_COLLECTION_OPTIONAL = "is_collection_optional";
+    public static final String OD_NAME_IS_SHARING_OPTIONAL = "is_sharing_optional";
+    public static final String OD_NAME_EPHEMERAL = "ephemeral";
+
+    public static final String TRUE_STR = "true";
+    public static final String FALSE_STR = "false";
+
+    /** Gets the single top-level {@link Element} having the {@param tagName}. */
+    public static Element getSingleElement(Document doc, String tagName) {
+        var elements = doc.getElementsByTagName(tagName);
+        return getSingleElement(elements, tagName);
+    }
+
+    /**
+     * Gets the single {@link Element} within {@param parentEle} and having the {@param tagName}.
+     */
+    public static Element getSingleElement(Element parentEle, String tagName) {
+        var elements = parentEle.getElementsByTagName(tagName);
+        return getSingleElement(elements, tagName);
+    }
+
+    /** Gets the single {@link Element} from {@param elements} and having the {@param tagName}. */
+    public static Element getSingleElement(NodeList elements, String tagName) {
+        if (elements.getLength() != 1) {
+            throw new IllegalArgumentException(
+                    String.format("Expected 1 %s but got %s.", tagName, elements.getLength()));
+        }
+        var elementAsNode = elements.item(0);
+        if (!(elementAsNode instanceof Element)) {
+            throw new IllegalStateException(String.format("%s was not an element.", tagName));
+        }
+        return ((Element) elementAsNode);
+    }
+
+    /** Gets the Boolean from the String value. */
+    public static Boolean fromString(String s) {
+        if (s == null) {
+            return null;
+        }
+        if (s.equals(TRUE_STR)) {
+            return true;
+        } else if (s.equals(FALSE_STR)) {
+            return false;
+        }
+        return null;
+    }
+
+    /** Creates an on-device PBundle DOM Element with the given attribute name. */
+    public static Element createPbundleEleWithName(Document doc, String name) {
+        var ele = doc.createElement(XmlUtils.OD_TAG_PBUNDLE_AS_MAP);
+        ele.setAttribute(XmlUtils.OD_ATTR_NAME, name);
+        return ele;
+    }
+
+    /** Create an on-device Boolean DOM Element with the given attribute name. */
+    public static Element createOdBooleanEle(Document doc, String name, boolean b) {
+        var ele = doc.createElement(XmlUtils.OD_TAG_BOOLEAN);
+        ele.setAttribute(XmlUtils.OD_ATTR_NAME, name);
+        ele.setAttribute(XmlUtils.OD_ATTR_VALUE, String.valueOf(b));
+        return ele;
+    }
+
+    /** Returns whether the String is null or empty. */
+    public static boolean isNullOrEmpty(String s) {
+        return s == null || s.isEmpty();
+    }
+}