Merge "Fix metro clock animation triggers twice when going to AOD" into main
diff --git a/apex/jobscheduler/OWNERS b/apex/jobscheduler/OWNERS
index 58434f1..22b6489 100644
--- a/apex/jobscheduler/OWNERS
+++ b/apex/jobscheduler/OWNERS
@@ -2,7 +2,6 @@
 ctate@google.com
 dplotnikov@google.com
 jji@google.com
-kwekua@google.com
 omakoto@google.com
 suprabh@google.com
 varunshah@google.com
diff --git a/apex/jobscheduler/service/aconfig/Android.bp b/apex/jobscheduler/service/aconfig/Android.bp
index 4db39dc..859c67a 100644
--- a/apex/jobscheduler/service/aconfig/Android.bp
+++ b/apex/jobscheduler/service/aconfig/Android.bp
@@ -2,6 +2,7 @@
 aconfig_declarations {
     name: "service-deviceidle.flags-aconfig",
     package: "com.android.server.deviceidle",
+    container: "system",
     srcs: [
         "device_idle.aconfig",
     ],
@@ -17,6 +18,7 @@
 aconfig_declarations {
     name: "service-job.flags-aconfig",
     package: "com.android.server.job",
+    container: "system",
     srcs: [
         "job.aconfig",
     ],
@@ -32,6 +34,7 @@
 aconfig_declarations {
     name: "alarm_flags",
     package: "com.android.server.alarm",
+    container: "system",
     srcs: ["alarm.aconfig"],
 }
 
diff --git a/apex/jobscheduler/service/aconfig/alarm.aconfig b/apex/jobscheduler/service/aconfig/alarm.aconfig
index bb0f3cb..d3068d7 100644
--- a/apex/jobscheduler/service/aconfig/alarm.aconfig
+++ b/apex/jobscheduler/service/aconfig/alarm.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.alarm"
+container: "system"
 
 flag {
     name: "use_frozen_state_to_drop_listener_alarms"
diff --git a/apex/jobscheduler/service/aconfig/device_idle.aconfig b/apex/jobscheduler/service/aconfig/device_idle.aconfig
index e4cb5ad..e8c99b1 100644
--- a/apex/jobscheduler/service/aconfig/device_idle.aconfig
+++ b/apex/jobscheduler/service/aconfig/device_idle.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.deviceidle"
+container: "system"
 
 flag {
     name: "disable_wakelocks_in_light_idle"
diff --git a/apex/jobscheduler/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig
index 5e6d377..75e2efd2 100644
--- a/apex/jobscheduler/service/aconfig/job.aconfig
+++ b/apex/jobscheduler/service/aconfig/job.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.job"
+container: "system"
 
 flag {
     name: "batch_active_bucket_jobs"
diff --git a/cmds/incident_helper/OWNERS b/cmds/incident_helper/OWNERS
index cede4ea..29f44ab 100644
--- a/cmds/incident_helper/OWNERS
+++ b/cmds/incident_helper/OWNERS
@@ -1,3 +1,2 @@
 joeo@google.com
-kwekua@google.com
 yanmin@google.com
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index b7c2ee9..b767c52 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -319,7 +319,6 @@
     field public static final String RECEIVE_DATA_ACTIVITY_CHANGE = "android.permission.RECEIVE_DATA_ACTIVITY_CHANGE";
     field public static final String RECEIVE_DEVICE_CUSTOMIZATION_READY = "android.permission.RECEIVE_DEVICE_CUSTOMIZATION_READY";
     field public static final String RECEIVE_EMERGENCY_BROADCAST = "android.permission.RECEIVE_EMERGENCY_BROADCAST";
-    field @FlaggedApi("android.permission.flags.voice_activation_permission_apis") public static final String RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA = "android.permission.RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA";
     field @FlaggedApi("android.permission.flags.voice_activation_permission_apis") public static final String RECEIVE_SANDBOX_TRIGGER_AUDIO = "android.permission.RECEIVE_SANDBOX_TRIGGER_AUDIO";
     field @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public static final String RECEIVE_SENSITIVE_NOTIFICATIONS = "android.permission.RECEIVE_SENSITIVE_NOTIFICATIONS";
     field public static final String RECEIVE_WIFI_CREDENTIAL_CHANGE = "android.permission.RECEIVE_WIFI_CREDENTIAL_CHANGE";
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 7ed10e7..7ae514a 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -1502,12 +1502,10 @@
             AppProtoEnums.APP_OP_RECEIVE_SANDBOX_TRIGGER_AUDIO;
 
     /**
-     * Allows the privileged assistant app to receive the training data from the sandboxed hotword
-     * detection service.
+     * This op has been deprecated.
      *
-     * @hide
      */
-    public static final int OP_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA =
+    private static final int OP_DEPRECATED_3 =
             AppProtoEnums.APP_OP_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA;
 
     /**
@@ -1735,7 +1733,6 @@
             OPSTR_CAMERA_SANDBOXED,
             OPSTR_RECORD_AUDIO_SANDBOXED,
             OPSTR_RECEIVE_SANDBOX_TRIGGER_AUDIO,
-            OPSTR_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA,
             OPSTR_CREATE_ACCESSIBILITY_OVERLAY,
             OPSTR_MEDIA_ROUTING_CONTROL,
             OPSTR_ENABLE_MOBILE_DATA_BY_USER,
@@ -2395,13 +2392,10 @@
             "android:receive_sandbox_trigger_audio";
 
     /**
-     * Allows the privileged assistant app to receive training data from the sandboxed hotword
-     * detection service.
-     *
+     * App op has been deprecated.
      * @hide
      */
-    public static final String OPSTR_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA =
-            "android:RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA";
+    public static final String OPSTR_DEPRECATED_3 = "android:deprecated_3";
 
     /**
      * Creation of an overlay using accessibility services
@@ -2582,7 +2576,6 @@
             OP_CAPTURE_CONSENTLESS_BUGREPORT_ON_USERDEBUG_BUILD,
             OP_USE_FULL_SCREEN_INTENT,
             OP_RECEIVE_SANDBOX_TRIGGER_AUDIO,
-            OP_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA,
             OP_MEDIA_ROUTING_CONTROL,
             OP_READ_SYSTEM_GRAMMATICAL_GENDER,
             OP_RUN_BACKUP_JOBS,
@@ -3021,11 +3014,8 @@
                 "RECEIVE_SANDBOX_TRIGGER_AUDIO")
                 .setPermission(Manifest.permission.RECEIVE_SANDBOX_TRIGGER_AUDIO)
                 .setDefaultMode(AppOpsManager.MODE_DEFAULT).build(),
-        new AppOpInfo.Builder(OP_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA,
-                OPSTR_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA,
-                "RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA")
-                .setPermission(Manifest.permission.RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA)
-                .setDefaultMode(AppOpsManager.MODE_DEFAULT).build(),
+        new AppOpInfo.Builder(OP_DEPRECATED_3, OPSTR_DEPRECATED_3, "DEPRECATED_3")
+                .setDefaultMode(AppOpsManager.MODE_IGNORED).build(),
         new AppOpInfo.Builder(OP_CREATE_ACCESSIBILITY_OVERLAY,
                 OPSTR_CREATE_ACCESSIBILITY_OVERLAY,
                 "CREATE_ACCESSIBILITY_OVERLAY")
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 42e82f6..ea6f45e 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -10284,6 +10284,16 @@
      * get the list of app restrictions set by each admin via
      * {@link android.content.RestrictionsManager#getApplicationRestrictionsPerAdmin}.
      *
+     * <p>Starting from Android Version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM},
+     * the device policy management role holder can also set app restrictions on any applications
+     * in the calling user, as well as the parent user of an organization-owned managed profile via
+     * the {@link DevicePolicyManager} instance returned by
+     * {@link #getParentProfileInstance(ComponentName)}. App restrictions set by the device policy
+     * management role holder are not returned by
+     * {@link UserManager#getApplicationRestrictions(String)}. The target application should use
+     * {@link android.content.RestrictionsManager#getApplicationRestrictionsPerAdmin} to retrieve
+     * them, alongside any app restrictions the profile or device owner might have set.
+     *
      * <p>NOTE: The method performs disk I/O and shouldn't be called on the main thread
      *
      * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or
@@ -10299,11 +10309,14 @@
     @WorkerThread
     public void setApplicationRestrictions(@Nullable ComponentName admin, String packageName,
             Bundle settings) {
-        throwIfParentInstance("setApplicationRestrictions");
+        if (!Flags.dmrhCanSetAppRestriction()) {
+            throwIfParentInstance("setApplicationRestrictions");
+        }
+
         if (mService != null) {
             try {
                 mService.setApplicationRestrictions(admin, mContext.getPackageName(), packageName,
-                        settings);
+                        settings, mParentInstance);
             } catch (RemoteException e) {
                 throw e.rethrowFromSystemServer();
             }
@@ -11704,11 +11717,14 @@
     @WorkerThread
     public @NonNull Bundle getApplicationRestrictions(
             @Nullable ComponentName admin, String packageName) {
-        throwIfParentInstance("getApplicationRestrictions");
+        if (!Flags.dmrhCanSetAppRestriction()) {
+            throwIfParentInstance("getApplicationRestrictions");
+        }
+
         if (mService != null) {
             try {
                 return mService.getApplicationRestrictions(admin, mContext.getPackageName(),
-                        packageName);
+                        packageName, mParentInstance);
             } catch (RemoteException e) {
                 throw e.rethrowFromSystemServer();
             }
@@ -13986,8 +14002,15 @@
     public @NonNull DevicePolicyManager getParentProfileInstance(@NonNull ComponentName admin) {
         throwIfParentInstance("getParentProfileInstance");
         try {
-            if (!mService.isManagedProfile(admin)) {
-                throw new SecurityException("The current user does not have a parent profile.");
+            if (Flags.dmrhCanSetAppRestriction()) {
+                UserManager um = mContext.getSystemService(UserManager.class);
+                if (!um.isManagedProfile()) {
+                    throw new SecurityException("The current user does not have a parent profile.");
+                }
+            } else {
+                if (!mService.isManagedProfile(admin)) {
+                    throw new SecurityException("The current user does not have a parent profile.");
+                }
             }
             return new DevicePolicyManager(mContext, mService, true);
         } catch (RemoteException e) {
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index d4589dc..2002326 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -244,8 +244,8 @@
     void setDefaultSmsApplication(in ComponentName admin, String callerPackageName, String packageName, boolean parent);
     void setDefaultDialerApplication(String packageName);
 
-    void setApplicationRestrictions(in ComponentName who, in String callerPackage, in String packageName, in Bundle settings);
-    Bundle getApplicationRestrictions(in ComponentName who, in String callerPackage, in String packageName);
+    void setApplicationRestrictions(in ComponentName who, in String callerPackage, in String packageName, in Bundle settings, in boolean parent);
+    Bundle getApplicationRestrictions(in ComponentName who, in String callerPackage, in String packageName, in boolean parent);
     boolean setApplicationRestrictionsManagingPackage(in ComponentName admin, in String packageName);
     String getApplicationRestrictionsManagingPackage(in ComponentName admin);
     boolean isCallerApplicationRestrictionsManagingPackage(in String callerPackage);
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index 6b2baa7..56fb4aa 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -204,6 +204,13 @@
 }
 
 flag {
+  name: "dmrh_can_set_app_restriction"
+  namespace: "enterprise"
+  description: "Allow DMRH to set application restrictions (both on the profile and the parent)"
+  bug: "328758346"
+}
+
+flag {
   name: "allow_screen_brightness_control_on_cope"
   namespace: "enterprise"
   description: "Allow COPE admin to control screen brightness and timeout."
diff --git a/core/java/android/app/assist/AssistStructure.java b/core/java/android/app/assist/AssistStructure.java
index 7548562..508077e 100644
--- a/core/java/android/app/assist/AssistStructure.java
+++ b/core/java/android/app/assist/AssistStructure.java
@@ -2655,13 +2655,12 @@
             );
         }
         GetCredentialRequest getCredentialRequest = node.getPendingCredentialRequest();
-        if (getCredentialRequest == null) {
-            Log.i(TAG, prefix + " No Credential Manager Request");
-        } else {
-            Log.i(TAG, prefix + "  GetCredentialRequest: no. of options= "
-                    + getCredentialRequest.getCredentialOptions().size()
-            );
-        }
+        Log.i(TAG, prefix + "  Credential Manager info:"
+                + " hasCredentialManagerRequest=" + (getCredentialRequest != null)
+                + (getCredentialRequest != null
+                        ? ", sizeOfOptions=" + getCredentialRequest.getCredentialOptions().size()
+                        : "")
+        );
 
         final int NCHILDREN = node.getChildCount();
         if (NCHILDREN > 0) {
diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java
index 90b7869..a0e40f6 100644
--- a/core/java/android/hardware/biometrics/BiometricPrompt.java
+++ b/core/java/android/hardware/biometrics/BiometricPrompt.java
@@ -52,6 +52,7 @@
 import android.util.Log;
 
 import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.FrameworkStatsLog;
 
 import java.lang.annotation.Retention;
@@ -70,7 +71,8 @@
 public class BiometricPrompt implements BiometricAuthenticator, BiometricConstants {
 
     private static final String TAG = "BiometricPrompt";
-    private static final int MAX_LOGO_DESCRIPTION_CHARACTER_NUMBER = 30;
+    @VisibleForTesting
+    static final int MAX_LOGO_DESCRIPTION_CHARACTER_NUMBER = 30;
 
     /**
      * Error/help message will show for this amount of time.
@@ -223,8 +225,8 @@
          *
          * @param logoDescription The logo description text that will be shown on the prompt.
          * @return This builder.
-         * @throws IllegalStateException If logo description is null or exceeds certain character
-         *                               limit.
+         * @throws IllegalArgumentException If logo description is null or exceeds certain character
+         *                                  limit.
          */
         @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
         @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
@@ -232,7 +234,7 @@
         public BiometricPrompt.Builder setLogoDescription(@NonNull String logoDescription) {
             if (logoDescription == null
                     || logoDescription.length() > MAX_LOGO_DESCRIPTION_CHARACTER_NUMBER) {
-                throw new IllegalStateException(
+                throw new IllegalArgumentException(
                         "Logo description passed in can not be null or exceed "
                                 + MAX_LOGO_DESCRIPTION_CHARACTER_NUMBER + " character number.");
             }
@@ -240,7 +242,6 @@
             return this;
         }
 
-
         /**
          * Required: Sets the title that will be shown on the prompt.
          * @param title The title to display.
diff --git a/core/java/android/hardware/biometrics/PromptContentViewWithMoreOptionsButton.java b/core/java/android/hardware/biometrics/PromptContentViewWithMoreOptionsButton.java
index 853d86c..a9eca3f 100644
--- a/core/java/android/hardware/biometrics/PromptContentViewWithMoreOptionsButton.java
+++ b/core/java/android/hardware/biometrics/PromptContentViewWithMoreOptionsButton.java
@@ -29,19 +29,22 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.util.concurrent.Executor;
 
 /**
- * Contains the information of the template of content view with a more options button for Biometric
- * Prompt.
+ * Contains the information of the template of content view with a more options button for
+ * Biometric Prompt.
+ * <p>
  * This button should be used to provide more options for sign in or other purposes, such as when a
  * user needs to select between multiple app-specific accounts or profiles that are available for
- * sign in. This is not common and apps should avoid using it if there is only one choice available
- * or if the user has already selected the appropriate account to use before invoking
- * BiometricPrompt because it will create additional steps that the user must navigate through.
- * Clicking the more options button will dismiss the prompt, provide the app an opportunity to ask
- * the user for the correct account, and finally allow the app to decide how to proceed once
- * selected.
+ * sign in.
+ * <p>
+ * Apps should avoid using this when possible because it will create additional steps that the user
+ * must navigate through - clicking the more options button will dismiss the prompt, provide the app
+ * an opportunity to ask the user for the correct option, and finally allow the app to decide how to
+ * proceed once selected.
  *
  * <p>
  * Here's how you'd set a <code>PromptContentViewWithMoreOptionsButton</code> on a Biometric
@@ -59,7 +62,8 @@
  */
 @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
 public final class PromptContentViewWithMoreOptionsButton implements PromptContentViewParcelable {
-    private static final int MAX_DESCRIPTION_CHARACTER_NUMBER = 225;
+    @VisibleForTesting
+    static final int MAX_DESCRIPTION_CHARACTER_NUMBER = 225;
 
     private final String mDescription;
     private DialogInterface.OnClickListener mListener;
@@ -132,14 +136,16 @@
         }
     };
 
+    /**
+     * A builder that collects arguments to be shown on the content view with more options button.
+     */
     public static final class Builder {
         private String mDescription;
         private Executor mExecutor;
         private DialogInterface.OnClickListener mListener;
 
         /**
-         * Optional: Sets a description that will be shown on the content view.  Note that there are
-         * limits on the number of characters allowed for description.
+         * Optional: Sets a description that will be shown on the content view.
          *
          * @param description The description to display.
          * @return This builder.
@@ -149,7 +155,7 @@
         @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED)
         public Builder setDescription(@NonNull String description) {
             if (description.length() > MAX_DESCRIPTION_CHARACTER_NUMBER) {
-                throw new IllegalStateException("The character number of description exceeds "
+                throw new IllegalArgumentException("The character number of description exceeds "
                         + MAX_DESCRIPTION_CHARACTER_NUMBER);
             }
             mDescription = description;
diff --git a/core/java/android/hardware/biometrics/PromptVerticalListContentView.java b/core/java/android/hardware/biometrics/PromptVerticalListContentView.java
index 02b2a50..d8b2867 100644
--- a/core/java/android/hardware/biometrics/PromptVerticalListContentView.java
+++ b/core/java/android/hardware/biometrics/PromptVerticalListContentView.java
@@ -24,6 +24,8 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.util.ArrayList;
 import java.util.List;
 
@@ -47,9 +49,12 @@
  */
 @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
 public final class PromptVerticalListContentView implements PromptContentViewParcelable {
-    private static final int MAX_ITEM_NUMBER = 20;
-    private static final int MAX_EACH_ITEM_CHARACTER_NUMBER = 640;
-    private static final int MAX_DESCRIPTION_CHARACTER_NUMBER = 225;
+    @VisibleForTesting
+    static final int MAX_ITEM_NUMBER = 20;
+    @VisibleForTesting
+    static final int MAX_EACH_ITEM_CHARACTER_NUMBER = 640;
+    @VisibleForTesting
+    static final int MAX_DESCRIPTION_CHARACTER_NUMBER = 225;
 
     private final List<PromptContentItemParcelable> mContentList;
     private final String mDescription;
@@ -155,7 +160,7 @@
         @NonNull
         public Builder setDescription(@NonNull String description) {
             if (description.length() > MAX_DESCRIPTION_CHARACTER_NUMBER) {
-                throw new IllegalStateException("The character number of description exceeds "
+                throw new IllegalArgumentException("The character number of description exceeds "
                         + MAX_DESCRIPTION_CHARACTER_NUMBER);
             }
             mDescription = description;
@@ -195,12 +200,12 @@
 
         private void checkItemLimits(@NonNull PromptContentItem listItem) {
             if (doesListItemExceedsCharLimit(listItem)) {
-                throw new IllegalStateException(
+                throw new IllegalArgumentException(
                         "The character number of list item exceeds "
                                 + MAX_EACH_ITEM_CHARACTER_NUMBER);
             }
             if (mContentList.size() > MAX_ITEM_NUMBER) {
-                throw new IllegalStateException(
+                throw new IllegalArgumentException(
                         "The number of list items exceeds " + MAX_ITEM_NUMBER);
             }
         }
diff --git a/core/java/android/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java
index ec9b013..dca663d 100644
--- a/core/java/android/hardware/camera2/CameraDevice.java
+++ b/core/java/android/hardware/camera2/CameraDevice.java
@@ -23,12 +23,14 @@
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
+import android.graphics.SurfaceTexture;
 import android.hardware.camera2.params.ExtensionSessionConfiguration;
 import android.hardware.camera2.params.InputConfiguration;
 import android.hardware.camera2.params.OutputConfiguration;
 import android.hardware.camera2.params.SessionConfiguration;
 import android.hardware.camera2.params.StreamConfigurationMap;
 import android.os.Handler;
+import android.util.Size;
 import android.view.Surface;
 
 import com.android.internal.camera.flags.Flags;
@@ -530,9 +532,10 @@
      *   SurfaceTexture}: Set the size of the SurfaceTexture with {@link
      *   android.graphics.SurfaceTexture#setDefaultBufferSize} to be one of the sizes returned by
      *   {@link StreamConfigurationMap#getOutputSizes(Class) getOutputSizes(SurfaceTexture.class)}
-     *   before creating a Surface from the SurfaceTexture with {@link Surface#Surface}. If the size
-     *   is not set by the application, it will be set to be the smallest supported size less than
-     *   1080p, by the camera device.</li>
+     *   before creating a Surface from the SurfaceTexture with
+     *   {@link Surface#Surface(SurfaceTexture)}. If the size is not set by the application,
+     *   it will be set to be the smallest supported size less than 1080p, by the camera
+     *   device.</li>
      *
      * <li>For recording with {@link android.media.MediaCodec}: Call
      *   {@link android.media.MediaCodec#createInputSurface} after configuring
@@ -1405,10 +1408,16 @@
      *
      * <p><b>NOTE:</b>
      * For apps targeting {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} and above,
-     * this method will ensure session parameters set through calls to
-     * {@link SessionConfiguration#setSessionParameters} are also supported if the Camera Device
-     * supports it. For apps targeting {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and
-     * below, session parameters will be ignored.</p>
+     * this method will automatically delegate to
+     * {@link CameraDeviceSetup#isSessionConfigurationSupported} whenever possible. This
+     * means that the output of this method will consider parameters set through
+     * {@link SessionConfiguration#setSessionParameters} as well.
+     * </p>
+     *
+     * <p>Session Parameters will be ignored for apps targeting <=
+     * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, or if
+     * {@link CameraManager#isCameraDeviceSetupSupported} returns false for the camera id
+     * associated with this {@code CameraDevice}.</p>
      *
      * @return {@code true} if the given session configuration is supported by the camera device
      *         {@code false} otherwise.
@@ -1419,6 +1428,8 @@
      *                               encountered a fatal error
      * @throws IllegalStateException if the camera device has been closed
      *
+     * @see CameraManager#isCameraDeviceSetupSupported(String)
+     * @see CameraDeviceSetup#isSessionConfigurationSupported(SessionConfiguration)
      */
     public boolean isSessionConfigurationSupported(
             @NonNull SessionConfiguration sessionConfig) throws CameraAccessException {
@@ -1703,7 +1714,7 @@
          * SessionConfiguration} can then be created using the OutputConfiguration objects and
          * be used to query whether it's supported by the camera device. To create the
          * CameraCaptureSession, the application still needs to make sure all output surfaces
-         * are added via {@link OutputConfiguration#addSurfaces} with the exception of deferred
+         * are added via {@link OutputConfiguration#addSurface} with the exception of deferred
          * surfaces for {@link android.view.SurfaceView} and
          * {@link android.graphics.SurfaceTexture}.</li>
          * </ul>
@@ -1751,7 +1762,7 @@
          * SessionConfiguration} can then be created using the OutputConfiguration objects and
          * be used for this function. To create the CameraCaptureSession, the application still
          * needs to make sure all output surfaces are added via {@link
-         * OutputConfiguration#addSurfaces} with the exception of deferred surfaces for {@link
+         * OutputConfiguration#addSurface} with the exception of deferred surfaces for {@link
          * android.view.SurfaceView} and {@link android.graphics.SurfaceTexture}.</p>
          *
          * @param sessionConfig The session configuration for which characteristics are fetched.
diff --git a/core/java/android/permission/TEST_MAPPING b/core/java/android/permission/TEST_MAPPING
index 69113ef..a15d9bc 100644
--- a/core/java/android/permission/TEST_MAPPING
+++ b/core/java/android/permission/TEST_MAPPING
@@ -11,5 +11,29 @@
                 }
             ]
         }
+    ],
+    "postsubmit": [
+        {
+            "name": "CtsVirtualDevicesAudioTestCases",
+            "options": [
+                {
+                    "exclude-annotation": "androidx.test.filters.FlakyTest"
+                },
+                {
+                    "include-filter": "android.virtualdevice.cts.audio.VirtualAudioPermissionTest"
+                }
+            ]
+        },
+        {
+            "name": "CtsVirtualDevicesAppLaunchTestCases",
+            "options": [
+                {
+                    "exclude-annotation": "androidx.test.filters.FlakyTest"
+                },
+                {
+                    "include-filter": "android.virtualdevice.cts.applaunch.VirtualDevicePermissionTest"
+                }
+            ]
+        }
     ]
 }
\ No newline at end of file
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 2402a0b..b52003f 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -31,7 +31,6 @@
 
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
 
-import android.animation.AnimationHandler;
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.TypeEvaluator;
@@ -69,7 +68,6 @@
 import android.view.inputmethod.InputMethodManager;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
 import com.android.internal.inputmethod.ImeTracing;
 import com.android.internal.inputmethod.SoftInputShowHideReason;
 import com.android.internal.util.function.TriFunction;
@@ -379,16 +377,6 @@
         private final WindowInsetsAnimationControlListener mLoggingListener;
         private final InputMethodJankContext mInputMethodJankContext;
 
-        private final ThreadLocal<AnimationHandler> mSfAnimationHandlerThreadLocal =
-                new ThreadLocal<AnimationHandler>() {
-            @Override
-            protected AnimationHandler initialValue() {
-                AnimationHandler handler = new AnimationHandler();
-                handler.setProvider(new SfVsyncFrameCallbackProvider());
-                return handler;
-            }
-        };
-
         public InternalAnimationControlListener(boolean show, boolean hasAnimationCallbacks,
                 @InsetsType int requestedTypes, @Behavior int behavior, boolean disable,
                 int floatingImeBottomInset, WindowInsetsAnimationControlListener loggingListener,
@@ -470,9 +458,6 @@
                     ImeTracker.forJank().onFinishAnimation(getAnimationType());
                 }
             });
-            if (!mHasAnimationCallbacks) {
-                mAnimator.setAnimationHandler(mSfAnimationHandlerThreadLocal.get());
-            }
             mAnimator.start();
         }
 
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index cfdf8fa..1cd7d34 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -1272,7 +1272,7 @@
      * surface has no buffer or crop, the surface is boundless and only constrained
      * by the size of its parent bounds.
      *
-     * @param session  The surface session, must not be null.
+     * @param session  The surface session.
      * @param name     The surface name, must not be null.
      * @param w        The surface initial width.
      * @param h        The surface initial height.
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index bd8e9c6..eb7de93 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -226,6 +226,7 @@
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Calendar;
@@ -3334,16 +3335,17 @@
     public static final int ACCESSIBILITY_LIVE_REGION_NONE = 0x00000000;
 
     /**
-     * Live region mode specifying that accessibility services should announce
-     * changes to this view.
+     * Live region mode specifying that accessibility services should notify users of changes to
+     * this view.
      * <p>
      * Use with {@link #setAccessibilityLiveRegion(int)}.
      */
     public static final int ACCESSIBILITY_LIVE_REGION_POLITE = 0x00000001;
 
     /**
-     * Live region mode specifying that accessibility services should interrupt
-     * ongoing speech to immediately announce changes to this view.
+     * Live region mode specifying that accessibility services should immediately notify users of
+     * changes to this view. For example, a screen reader may interrupt ongoing speech to
+     * immediately announce these changes.
      * <p>
      * Use with {@link #setAccessibilityLiveRegion(int)}.
      */
@@ -8797,14 +8799,17 @@
      *
      * <p>
      * When transitioning from one Activity to another, instead of using
-     * setAccessibilityPaneTitle(), set a descriptive title for its window by using android:label
-     * for the matching <activity> entry in your application’s manifest or updating the title at
-     * runtime with{@link android.app.Activity#setTitle(CharSequence)}.
+     * {@code setAccessibilityPaneTitle()}, set a descriptive title for its window by using
+     * {@code android:label}
+     * for the matching Activity entry in your application's manifest or updating the title at
+     * runtime with {@link android.app.Activity#setTitle(CharSequence)}.
      *
      * <p>
+     * <aside>
      * <b>Note:</b> Use
      * {@link androidx.core.view.ViewCompat#setAccessibilityPaneTitle(View, CharSequence)}
-     * for backwards-compatibility. </aside>
+     * for backwards-compatibility.
+     * </aside>
      * @param accessibilityPaneTitle The pane's title. Setting to {@code null} indicates that this
      *                               View is not a pane.
      *
@@ -8912,7 +8917,7 @@
      * They should not need to specify what exactly is announced to users.
      *
      * <p>
-     * In general, only announce transitions and don’t generate a confirmation message for simple
+     * In general, only announce transitions and don't generate a confirmation message for simple
      * actions like a button press. Label your controls concisely and precisely instead, and for
      * significant UI changes like window changes, use
      * {@link android.app.Activity#setTitle(CharSequence)} and
@@ -15305,33 +15310,56 @@
      * to the view's content description or text, or to the content descriptions
      * or text of the view's children (where applicable).
      * <p>
-     * To indicate that the user should be notified of changes, use
-     * {@link #ACCESSIBILITY_LIVE_REGION_POLITE}. Announcements from this region are queued and
-     * do not disrupt ongoing speech.
+     * Different priority levels are available:
+     * <ul>
+     *   <li>
+     *       {@link #ACCESSIBILITY_LIVE_REGION_POLITE}:
+     *       Indicates that updates to the region should be presented to the user. Suitable in most
+     *       cases for prominent updates within app content that don't require the user's immediate
+     *       attention.
+     *   </li>
+     *   <li>
+     *       {@link #ACCESSIBILITY_LIVE_REGION_ASSERTIVE}: Indicates that updates to the region have
+     *       the highest priority and should be presented to the user immediately. This may result
+     *       in disruptive notifications from an accessibility service, which may potentially
+     *       interrupt other feedback or user actions, so it should generally be used only for
+     *       critical, time-sensitive information.
+     *   </li>
+     *   <li>
+     *       {@link #ACCESSIBILITY_LIVE_REGION_NONE}: Disables change announcements (the default for
+     *       most views).
+     *   </li>
+     * </ul>
      * <p>
-     * For example, selecting an option in a dropdown menu may update a panel below with the updated
-     * content. This panel may be marked as a live region with
-     * {@link #ACCESSIBILITY_LIVE_REGION_POLITE} to notify users of the change.
+     * Examples:
+     * <ul>
+     *     <li>
+     *         Selecting an option in a dropdown menu updates a panel below with the updated
+     *         content. This panel may be marked as a live region with
+     *         {@link #ACCESSIBILITY_LIVE_REGION_POLITE} to notify users of the change. A screen
+     *         reader may queue changes as announcements that don't disrupt ongoing speech.
+     *      </li>
+     *      <li>
+     *          An emergency alert may be marked with {@link #ACCESSIBILITY_LIVE_REGION_ASSERTIVE}
+     *          to immediately inform users of the emergency.
+     *      </li>
+     * </ul>
      * <p>
-     * For notifying users about errors, such as in a login screen with text that displays an
-     * "incorrect password" notification, that view should send an AccessibilityEvent of type
+     * For error notifications, like an "incorrect password" warning in a login screen, views
+     * should send a {@link AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED}
+     * {@code AccessibilityEvent} with a content change type
      * {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_ERROR} and set
-     * {@link AccessibilityNodeInfo#setError(CharSequence)} instead. Custom widgets should expose
-     * error-setting methods that support accessibility automatically. For example, instead of
-     * explicitly sending this event when using a TextView, use
-     * {@link android.widget.TextView#setError(CharSequence)}.
+     * {@link AccessibilityNodeInfo#setError(CharSequence)}. Custom widgets should provide
+     * error-setting methods that support accessibility. For example, use
+     * {@link android.widget.TextView#setError(CharSequence)} instead of explicitly sending events.
      * <p>
-     * To disable change notifications for this view, use
-     * {@link #ACCESSIBILITY_LIVE_REGION_NONE}. This is the default live region
-     * mode for most views.
+     * Don't use live regions for frequently-updating UI elements (e.g., progress bars), as this can
+     * overwhelm the user with feedback from accessibility services. If necessary, use
+     * {@link AccessibilityNodeInfo#setMinDurationBetweenContentChanges(Duration)} to throttle
+     * feedback and reduce disruptions.
      * <p>
-     * If the view's changes should interrupt ongoing speech and notify the user
-     * immediately, use {@link #ACCESSIBILITY_LIVE_REGION_ASSERTIVE}. This may result in disruptive
-     * announcements from an accessibility service, so it should generally be used only to convey
-     * information that is time-sensitive or critical for use of the application. Examples may
-     * include an incoming call or an emergency alert.
-     * <p>
-     * <b>Note:</b> Use {@link androidx.core.view.ViewCompat#setAccessibilityLiveRegion(View, int)}
+     * <aside><b>Note:</b> Use
+     * {@link androidx.core.view.ViewCompat#setAccessibilityLiveRegion(View, int)}
      * for backwards-compatibility. </aside>
      *
      * @param mode The live region mode for this view, one of:
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index c8cb1a7..db1b73f 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -110,6 +110,7 @@
 import static android.view.flags.Flags.toolkitMetricsForFrameRateDecision;
 import static android.view.flags.Flags.toolkitSetFrameRateReadOnly;
 import static android.view.flags.Flags.toolkitFrameRateFunctionEnablingReadOnly;
+import static android.view.flags.Flags.toolkitFrameRateViewEnablingReadOnly;
 import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.IME_FOCUS_CONTROLLER;
 import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.INSETS_CONTROLLER;
 
@@ -1153,6 +1154,7 @@
     private static boolean sToolkitFrameRateFunctionEnablingReadOnlyFlagValue;
     private static boolean sToolkitMetricsForFrameRateDecisionFlagValue;
     private static boolean sToolkitFrameRateTypingReadOnlyFlagValue;
+    private static final boolean sToolkitFrameRateViewEnablingReadOnlyFlagValue;
     private static boolean sToolkitFrameRateVelocityMappingReadOnlyFlagValue =
             toolkitFrameRateVelocityMappingReadOnly();;
 
@@ -1162,6 +1164,8 @@
         sToolkitFrameRateTypingReadOnlyFlagValue = toolkitFrameRateTypingReadOnly();
         sToolkitFrameRateFunctionEnablingReadOnlyFlagValue =
                 toolkitFrameRateFunctionEnablingReadOnly();
+        sToolkitFrameRateViewEnablingReadOnlyFlagValue =
+                toolkitFrameRateViewEnablingReadOnly();
     }
 
     // The latest input event from the gesture that was used to resolve the pointer icon.
@@ -2624,8 +2628,10 @@
         // no longer needed if the dVRR feature is disabled.
         if (shouldEnableDvrr()) {
             try {
-                mFrameRateTransaction.setFrameRateSelectionStrategy(sc,
+                if (sToolkitFrameRateFunctionEnablingReadOnlyFlagValue) {
+                    mFrameRateTransaction.setFrameRateSelectionStrategy(sc,
                         sc.FRAME_RATE_SELECTION_STRATEGY_SELF).applyAsyncUnsafe();
+                }
             } catch (Exception e) {
                 Log.e(mTag, "Unable to set frame rate selection strategy ", e);
             }
@@ -12536,9 +12542,11 @@
                                     + category + ", reason " + reason + ", "
                                     + sourceView);
                 }
-                mFrameRateTransaction.setFrameRateCategory(mSurfaceControl,
+                if (sToolkitFrameRateFunctionEnablingReadOnlyFlagValue) {
+                    mFrameRateTransaction.setFrameRateCategory(mSurfaceControl,
                         frameRateCategory, false).applyAsyncUnsafe();
-                mLastPreferredFrameRateCategory = frameRateCategory;
+                    mLastPreferredFrameRateCategory = frameRateCategory;
+                }
             }
         } catch (Exception e) {
             Log.e(mTag, "Unable to set frame rate category", e);
@@ -12595,9 +12603,11 @@
                                 + preferredFrameRate + " compatibility "
                                 + mFrameRateCompatibility);
                 }
-                mFrameRateTransaction.setFrameRate(mSurfaceControl, preferredFrameRate,
+                if (sToolkitFrameRateFunctionEnablingReadOnlyFlagValue) {
+                    mFrameRateTransaction.setFrameRate(mSurfaceControl, preferredFrameRate,
                     mFrameRateCompatibility).applyAsyncUnsafe();
-                mLastPreferredFrameRate = preferredFrameRate;
+                    mLastPreferredFrameRate = preferredFrameRate;
+                }
             }
         } catch (Exception e) {
             Log.e(mTag, "Unable to set frame rate", e);
@@ -12824,7 +12834,7 @@
 
     private boolean shouldEnableDvrr() {
         // uncomment this when we are ready for enabling dVRR
-        if (sToolkitFrameRateFunctionEnablingReadOnlyFlagValue) {
+        if (sToolkitFrameRateViewEnablingReadOnlyFlagValue) {
             return sToolkitSetFrameRateReadOnlyFlagValue && isFrameRatePowerSavingsBalanced();
         }
         return false;
diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
index 03ba8ae..a5ba294 100644
--- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
@@ -159,7 +159,7 @@
      * <p> To avoid disconnected trees, this flag will also prefetch the parent. Siblings will be
      * prefetched before descendants.
      *
-     * @see #FLAG_PREFETCH_ANCESTORS for where to use these flags.
+     * <p> See {@link #FLAG_PREFETCH_ANCESTORS} for information on where these flags can be used.
      */
     public static final int FLAG_PREFETCH_SIBLINGS = 1 << 1;
 
@@ -171,7 +171,7 @@
      * {@link #FLAG_PREFETCH_DESCENDANTS_BREADTH_FIRST} or this will trigger an
      * IllegalArgumentException.
      *
-     * @see #FLAG_PREFETCH_ANCESTORS for where to use these flags.
+     * <p> See {@link #FLAG_PREFETCH_ANCESTORS} for information on where these flags can be used.
      */
     public static final int FLAG_PREFETCH_DESCENDANTS_HYBRID = 1 << 2;
 
@@ -181,7 +181,7 @@
      * {@link #FLAG_PREFETCH_DESCENDANTS_BREADTH_FIRST} or this will trigger an
      * IllegalArgumentException.
      *
-     * @see #FLAG_PREFETCH_ANCESTORS for where to use these flags.
+     * <p> See {@link #FLAG_PREFETCH_ANCESTORS} for information on where these flags can be used.
      */
     public static final int FLAG_PREFETCH_DESCENDANTS_DEPTH_FIRST = 1 << 3;
 
@@ -191,7 +191,7 @@
      * {@link #FLAG_PREFETCH_DESCENDANTS_DEPTH_FIRST} or this will trigger an
      * IllegalArgumentException.
      *
-     * @see #FLAG_PREFETCH_ANCESTORS for where to use these flags.
+     * <p> See {@link #FLAG_PREFETCH_ANCESTORS} for information on where these flags can be used.
      */
     public static final int FLAG_PREFETCH_DESCENDANTS_BREADTH_FIRST = 1 << 4;
 
@@ -199,7 +199,7 @@
      * Prefetching flag that specifies prefetching should not be interrupted by a request to
      * retrieve a node or perform an action on a node.
      *
-     * @see #FLAG_PREFETCH_ANCESTORS for where to use these flags.
+     * <p> See {@link #FLAG_PREFETCH_ANCESTORS} for information on where these flags can be used.
      */
     public static final int FLAG_PREFETCH_UNINTERRUPTIBLE = 1 << 5;
 
@@ -1295,6 +1295,8 @@
     /**
      * Get the child at given index.
      *
+     * <p>
+     * See {@link #getParent(int)} for a description of prefetching.
      * @param index The child index.
      * @param prefetchingStrategy the prefetching strategy.
      * @return The child node.
@@ -1302,7 +1304,6 @@
      * @throws IllegalStateException If called outside of an {@link AccessibilityService} and before
      *                               calling {@link #setQueryFromAppProcessEnabled}.
      *
-     * @see AccessibilityNodeInfo#getParent(int) for a description of prefetching.
      */
     @Nullable
     public AccessibilityNodeInfo getChild(int index, @PrefetchingStrategy int prefetchingStrategy) {
@@ -1903,8 +1904,13 @@
      * Accessibility service will throttle those content change events and only handle one event
      * per minute for that view.
      * </p>
+     * <p>
+     * Example UI elements that frequently update and may benefit from a duration are progress bars,
+     * timers, and stopwatches.
+     * </p>
      *
-     * @see AccessibilityEvent#getContentChangeTypes for all content change types.
+     * @see AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED
+     * @see AccessibilityEvent#getContentChangeTypes
      * @param duration the minimum duration between content change events.
      *                                         Negative duration would be treated as zero.
      */
@@ -3954,7 +3960,7 @@
     /**
      * Returns the container title.
      *
-     * @see #setContainerTitle for details.
+     * @see #setContainerTitle
      */
     @Nullable
     public CharSequence getContainerTitle() {
@@ -5187,8 +5193,8 @@
          * <p>The node that is focused should return {@code true} for
          * {@link AccessibilityNodeInfo#isFocused()}.
          *
-         * @see #ACTION_ACCESSIBILITY_FOCUS for the difference between system and accessibility
-         * focus.
+         * See {@link #ACTION_ACCESSIBILITY_FOCUS} for the difference between system and
+         * accessibility focus.
          */
         public static final AccessibilityAction ACTION_FOCUS =
                 new AccessibilityAction(AccessibilityNodeInfo.ACTION_FOCUS);
diff --git a/core/java/android/widget/ProgressBar.java b/core/java/android/widget/ProgressBar.java
index 356d059..6e43d0f 100644
--- a/core/java/android/widget/ProgressBar.java
+++ b/core/java/android/widget/ProgressBar.java
@@ -69,6 +69,7 @@
 import com.android.internal.R;
 
 import java.text.NumberFormat;
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Locale;
 
@@ -139,6 +140,14 @@
  * <p>The "inverse" styles provide an inverse color scheme for the spinner, which may be necessary
  * if your application uses a light colored theme (a white background).</p>
  *
+ * <h4>Accessibility</h4>
+ * <p>
+ * Consider using
+ * {@link AccessibilityNodeInfo#setMinDurationBetweenContentChanges(Duration)} to
+ * convey to accessibility services that changes can be throttled. This may reduce the
+ * frequency of potentially disruptive notifications.
+ * </p>
+ *
  * <p><strong>XML attributes</b></strong>
  * <p>
  * See {@link android.R.styleable#ProgressBar ProgressBar Attributes},
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 4402ac7..dd6b772a 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -9,6 +9,16 @@
 }
 
 flag {
+    name: "wait_for_transition_on_display_switch"
+    namespace: "windowing_frontend"
+    description: "Waits for Shell transition to start before unblocking the screen after display switch"
+    bug: "301420598"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
   name: "edge_to_edge_by_default"
   namespace: "windowing_frontend"
   description: "Make app go edge-to-edge by default when targeting SDK 35 or greater"
diff --git a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
index 9481dc9..a0c405e 100644
--- a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
+++ b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
@@ -53,7 +53,6 @@
 import android.view.Window;
 import android.view.WindowManager;
 import android.view.accessibility.AccessibilityManager;
-import android.view.accessibility.Flags;
 import android.widget.Toast;
 
 import com.android.internal.R;
@@ -369,23 +368,17 @@
                         })
                 .setPositiveButton(R.string.accessibility_shortcut_off,
                         (DialogInterface d, int which) -> {
-                            if (Flags.updateAlwaysOnA11yService()) {
-                                Set<String> targetServices =
-                                        ShortcutUtils.getShortcutTargetsFromSettings(
-                                                mContext,
-                                                HARDWARE,
-                                                userId);
+                            Set<String> targetServices =
+                                    ShortcutUtils.getShortcutTargetsFromSettings(
+                                            mContext,
+                                            HARDWARE,
+                                            userId);
 
-                                Settings.Secure.putStringForUser(mContext.getContentResolver(),
-                                        Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, "",
-                                        userId);
-                                ShortcutUtils.updateInvisibleToggleAccessibilityServiceEnableState(
-                                        mContext, targetServices, userId);
-                            } else {
-                                Settings.Secure.putStringForUser(mContext.getContentResolver(),
-                                        Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, "",
-                                        userId);
-                            }
+                            Settings.Secure.putStringForUser(mContext.getContentResolver(),
+                                    Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, "",
+                                    userId);
+                            ShortcutUtils.updateInvisibleToggleAccessibilityServiceEnableState(
+                                    mContext, targetServices, userId);
 
                             // If canceled, treat as if the dialog has never been shown
                             Settings.Secure.putIntForUser(mContext.getContentResolver(),
diff --git a/core/java/com/android/internal/accessibility/dialog/InvisibleToggleAccessibilityServiceTarget.java b/core/java/com/android/internal/accessibility/dialog/InvisibleToggleAccessibilityServiceTarget.java
index 7831afb..2097788 100644
--- a/core/java/com/android/internal/accessibility/dialog/InvisibleToggleAccessibilityServiceTarget.java
+++ b/core/java/com/android/internal/accessibility/dialog/InvisibleToggleAccessibilityServiceTarget.java
@@ -16,17 +16,11 @@
 
 package com.android.internal.accessibility.dialog;
 
-import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE;
-import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE;
-import static com.android.internal.accessibility.util.AccessibilityUtils.setAccessibilityServiceState;
-import static com.android.internal.accessibility.util.ShortcutUtils.isComponentIdExistingInSettings;
-
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.annotation.NonNull;
 import android.content.ComponentName;
 import android.content.Context;
 import android.os.UserHandle;
-import android.view.accessibility.Flags;
 
 import com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType;
 import com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType;
@@ -53,31 +47,9 @@
 
     @Override
     public void onCheckedChanged(boolean isChecked) {
+        super.onCheckedChanged(isChecked);
         final ComponentName componentName = ComponentName.unflattenFromString(getId());
-
-        if (Flags.updateAlwaysOnA11yService()) {
-            super.onCheckedChanged(isChecked);
-            ShortcutUtils.updateInvisibleToggleAccessibilityServiceEnableState(
-                    getContext(), Set.of(componentName.flattenToString()), UserHandle.myUserId());
-        } else {
-            if (!isComponentIdExistingInOtherShortcut()) {
-                setAccessibilityServiceState(getContext(), componentName, isChecked);
-            }
-
-            super.onCheckedChanged(isChecked);
-        }
-    }
-
-    private boolean isComponentIdExistingInOtherShortcut() {
-        switch (getShortcutType()) {
-            case SOFTWARE:
-                return isComponentIdExistingInSettings(getContext(), UserShortcutType.HARDWARE,
-                        getId());
-            case HARDWARE:
-                return isComponentIdExistingInSettings(getContext(), UserShortcutType.SOFTWARE,
-                        getId());
-            default:
-                throw new IllegalStateException("Unexpected shortcut type");
-        }
+        ShortcutUtils.updateInvisibleToggleAccessibilityServiceEnableState(
+                getContext(), Set.of(componentName.flattenToString()), UserHandle.myUserId());
     }
 }
diff --git a/core/java/com/android/internal/compat/Android.bp b/core/java/com/android/internal/compat/Android.bp
index 9ff05a6..8253aa8 100644
--- a/core/java/com/android/internal/compat/Android.bp
+++ b/core/java/com/android/internal/compat/Android.bp
@@ -1,6 +1,7 @@
 aconfig_declarations {
     name: "compat_logging_flags",
     package: "com.android.internal.compat.flags",
+    container: "system",
     srcs: [
         "compat_logging_flags.aconfig",
     ],
diff --git a/core/java/com/android/internal/compat/compat_logging_flags.aconfig b/core/java/com/android/internal/compat/compat_logging_flags.aconfig
index a5c31ed..4f51626 100644
--- a/core/java/com/android/internal/compat/compat_logging_flags.aconfig
+++ b/core/java/com/android/internal/compat/compat_logging_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.internal.compat.flags"
+container: "system"
 
 flag {
     name: "skip_old_and_disabled_compat_logging"
@@ -6,4 +7,4 @@
     description: "Feature flag for skipping debug logging for changes that do not target the latest sdk or are disabled"
     bug: "323949942"
     is_fixed_read_only: true
-}
\ No newline at end of file
+}
diff --git a/core/java/com/android/internal/foldables/Android.bp b/core/java/com/android/internal/foldables/Android.bp
index f1d06da..53a9be5 100644
--- a/core/java/com/android/internal/foldables/Android.bp
+++ b/core/java/com/android/internal/foldables/Android.bp
@@ -1,6 +1,7 @@
 aconfig_declarations {
     name: "fold_lock_setting_flags",
     package: "com.android.internal.foldables.flags",
+    container: "system",
     srcs: [
         "fold_lock_setting_flags.aconfig",
     ],
diff --git a/core/java/com/android/internal/foldables/fold_lock_setting_flags.aconfig b/core/java/com/android/internal/foldables/fold_lock_setting_flags.aconfig
index d73e623..03b6a5b 100644
--- a/core/java/com/android/internal/foldables/fold_lock_setting_flags.aconfig
+++ b/core/java/com/android/internal/foldables/fold_lock_setting_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.internal.foldables.flags"
+container: "system"
 
 flag {
     name: "fold_lock_setting_enabled"
diff --git a/core/jni/android_text_Hyphenator.cpp b/core/jni/android_text_Hyphenator.cpp
index 7c976b7..b6bf617 100644
--- a/core/jni/android_text_Hyphenator.cpp
+++ b/core/jni/android_text_Hyphenator.cpp
@@ -36,43 +36,41 @@
     return SYSTEM_HYPHENATOR_PREFIX + lowerLocale + SYSTEM_HYPHENATOR_SUFFIX;
 }
 
-static std::pair<const uint8_t*, uint32_t> mmapPatternFile(const std::string& locale) {
+static const uint8_t* mmapPatternFile(const std::string& locale) {
     const std::string hyFilePath = buildFileName(locale);
     const int fd = open(hyFilePath.c_str(), O_RDONLY | O_CLOEXEC);
     if (fd == -1) {
-        return std::make_pair(nullptr, 0); // Open failed.
+        return nullptr;  // Open failed.
     }
 
     struct stat st = {};
     if (fstat(fd, &st) == -1) {  // Unlikely to happen.
         close(fd);
-        return std::make_pair(nullptr, 0);
+        return nullptr;
     }
 
     void* ptr = mmap(nullptr, st.st_size, PROT_READ, MAP_SHARED, fd, 0 /* offset */);
     close(fd);
     if (ptr == MAP_FAILED) {
-        return std::make_pair(nullptr, 0);
+        return nullptr;
     }
-    return std::make_pair(reinterpret_cast<const uint8_t*>(ptr), st.st_size);
+    return reinterpret_cast<const uint8_t*>(ptr);
 }
 
 static void addHyphenatorWithoutPatternFile(const std::string& locale, int minPrefix,
         int minSuffix) {
-    minikin::addHyphenator(locale,
-                           minikin::Hyphenator::loadBinary(nullptr, 0, minPrefix, minSuffix,
-                                                           locale));
+    minikin::addHyphenator(locale, minikin::Hyphenator::loadBinary(
+            nullptr, minPrefix, minSuffix, locale));
 }
 
 static void addHyphenator(const std::string& locale, int minPrefix, int minSuffix) {
-    auto [ptr, size] = mmapPatternFile(locale);
+    const uint8_t* ptr = mmapPatternFile(locale);
     if (ptr == nullptr) {
         ALOGE("Unable to find pattern file or unable to map it for %s", locale.c_str());
         return;
     }
-    minikin::addHyphenator(locale,
-                           minikin::Hyphenator::loadBinary(ptr, size, minPrefix, minSuffix,
-                                                           locale));
+    minikin::addHyphenator(locale, minikin::Hyphenator::loadBinary(
+            ptr, minPrefix, minSuffix, locale));
 }
 
 static void addHyphenatorAlias(const std::string& from, const std::string& to) {
diff --git a/core/jni/android_tracing_PerfettoDataSource.cpp b/core/jni/android_tracing_PerfettoDataSource.cpp
index 1eff5ce..25ff853 100644
--- a/core/jni/android_tracing_PerfettoDataSource.cpp
+++ b/core/jni/android_tracing_PerfettoDataSource.cpp
@@ -213,7 +213,7 @@
 
 PerfettoDataSource::~PerfettoDataSource() {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
-    env->DeleteWeakGlobalRef(mJavaDataSource);
+    env->DeleteGlobalRef(mJavaDataSource);
 }
 
 jlong nativeCreate(JNIEnv* env, jclass clazz, jobject javaDataSource, jstring name) {
diff --git a/core/proto/OWNERS b/core/proto/OWNERS
index b900fa6..b51f72d 100644
--- a/core/proto/OWNERS
+++ b/core/proto/OWNERS
@@ -11,7 +11,6 @@
 # Frameworks
 ogunwale@google.com
 jjaggi@google.com
-kwekua@google.com
 roosa@google.com
 per-file package_item_info.proto = file:/PACKAGE_MANAGER_OWNERS
 per-file usagestatsservice.proto, usagestatsservice_v2.proto = file:/core/java/android/app/usage/OWNERS
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index d4256ca..f743299 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -7507,16 +7507,6 @@
     <permission android:name="android.permission.RECEIVE_SANDBOX_TRIGGER_AUDIO"
                 android:protectionLevel="signature|privileged|appop" />
 
-    <!-- @SystemApi Required for the privileged assistant apps targeting
-         {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}
-         that receive training data from a sandboxed {@link HotwordDetectionService} or
-         {@link VisualQueryDetectionService}.
-         <p>Protection level: internal|appop
-         @FlaggedApi("android.permission.flags.voice_activation_permission_apis")
-         @hide -->
-    <permission android:name="android.permission.RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA"
-                android:protectionLevel="internal|appop" />
-
     <!-- @SystemApi Allows requesting the framework broadcast the
          {@link Intent#ACTION_DEVICE_CUSTOMIZATION_READY} intent.
          @hide -->
diff --git a/core/tests/coretests/src/android/hardware/biometrics/BiometricPromptTest.java b/core/tests/coretests/src/android/hardware/biometrics/BiometricPromptTest.java
index 66f3bca..ca91542 100644
--- a/core/tests/coretests/src/android/hardware/biometrics/BiometricPromptTest.java
+++ b/core/tests/coretests/src/android/hardware/biometrics/BiometricPromptTest.java
@@ -16,6 +16,14 @@
 
 package android.hardware.biometrics;
 
+import static android.hardware.biometrics.BiometricPrompt.MAX_LOGO_DESCRIPTION_CHARACTER_NUMBER;
+import static android.hardware.biometrics.PromptContentViewWithMoreOptionsButton.MAX_DESCRIPTION_CHARACTER_NUMBER;
+import static android.hardware.biometrics.PromptVerticalListContentView.MAX_EACH_ITEM_CHARACTER_NUMBER;
+import static android.hardware.biometrics.PromptVerticalListContentView.MAX_ITEM_NUMBER;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
@@ -40,6 +48,7 @@
 import org.mockito.junit.MockitoJUnitRunner;
 import org.mockito.junit.MockitoRule;
 
+import java.util.Random;
 import java.util.concurrent.Executor;
 
 
@@ -83,10 +92,11 @@
                 ArgumentCaptor.forClass(IBiometricServiceReceiver.class);
         BiometricPrompt.AuthenticationCallback callback =
                 new BiometricPrompt.AuthenticationCallback() {
-            @Override
-            public void onAuthenticationError(int errorCode, CharSequence errString) {
-                super.onAuthenticationError(errorCode, errString);
-            }};
+                    @Override
+                    public void onAuthenticationError(int errorCode, CharSequence errString) {
+                        super.onAuthenticationError(errorCode, errString);
+                    }
+                };
         mBiometricPrompt.authenticate(mCancellationSignal, mExecutor, callback);
         mLooper.dispatchAll();
 
@@ -99,4 +109,112 @@
 
         verify(mService).cancelAuthentication(any(), anyString(), anyLong());
     }
+
+    @Test
+    public void testLogoDescription_null() {
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> new BiometricPrompt.Builder(mContext).setLogoDescription(null)
+        );
+
+        assertThat(e).hasMessageThat().contains(
+                "Logo description passed in can not be null or exceed");
+    }
+
+    @Test
+    public void testLogoDescription_charLimit() {
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> new BiometricPrompt.Builder(mContext).setLogoDescription(
+                        generateRandomString(MAX_LOGO_DESCRIPTION_CHARACTER_NUMBER + 1))
+        );
+
+        assertThat(e).hasMessageThat().contains(
+                "Logo description passed in can not be null or exceed");
+    }
+
+    @Test
+    public void testMoreOptionsButton_descriptionCharLimit() {
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> new PromptContentViewWithMoreOptionsButton.Builder().setDescription(
+                        generateRandomString(MAX_DESCRIPTION_CHARACTER_NUMBER + 1))
+        );
+
+        assertThat(e).hasMessageThat().contains(
+                "The character number of description exceeds ");
+    }
+
+    @Test
+    public void testMoreOptionsButton_ExecutorNull() {
+        PromptContentViewWithMoreOptionsButton.Builder builder =
+                new PromptContentViewWithMoreOptionsButton.Builder().setMoreOptionsButtonListener(
+                        null, null);
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                builder::build
+        );
+
+        assertThat(e).hasMessageThat().contains(
+                "The executor for the listener of more options button on prompt content must be "
+                        + "set");
+    }
+
+    @Test
+    public void testMoreOptionsButton_ListenerNull() {
+        PromptContentViewWithMoreOptionsButton.Builder builder =
+                new PromptContentViewWithMoreOptionsButton.Builder().setMoreOptionsButtonListener(
+                        mExecutor, null);
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                builder::build
+        );
+
+        assertThat(e).hasMessageThat().contains(
+                "The listener of more options button on prompt content must be set");
+    }
+
+    @Test
+    public void testVerticalList_descriptionCharLimit() {
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> new PromptVerticalListContentView.Builder().setDescription(
+                        generateRandomString(MAX_DESCRIPTION_CHARACTER_NUMBER + 1))
+        );
+
+        assertThat(e).hasMessageThat().contains(
+                "The character number of description exceeds ");
+    }
+
+    @Test
+    public void testVerticalList_itemCharLimit() {
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> new PromptVerticalListContentView.Builder().addListItem(
+                        new PromptContentItemBulletedText(
+                                generateRandomString(MAX_EACH_ITEM_CHARACTER_NUMBER + 1)))
+        );
+
+        assertThat(e).hasMessageThat().contains(
+                "The character number of list item exceeds ");
+    }
+
+    @Test
+    public void testVerticalList_itemNumLimit() {
+        PromptVerticalListContentView.Builder builder = new PromptVerticalListContentView.Builder();
+
+        for (int i = 0; i < MAX_ITEM_NUMBER; i++) {
+            builder.addListItem(new PromptContentItemBulletedText(generateRandomString(10)));
+        }
+
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> builder.addListItem(
+                        new PromptContentItemBulletedText(generateRandomString(10)))
+        );
+
+        assertThat(e).hasMessageThat().contains(
+                "The number of list items exceeds ");
+    }
+
+    private String generateRandomString(int charNum) {
+        final Random random = new Random();
+        final StringBuilder longString = new StringBuilder(charNum);
+        for (int j = 0; j < charNum; j++) {
+            longString.append(random.nextInt(10));
+        }
+        return longString.toString();
+    }
 }
diff --git a/core/tests/coretests/src/android/view/MotionEventTest.java b/core/tests/coretests/src/android/view/MotionEventTest.java
index c4c983d..6a6e652 100644
--- a/core/tests/coretests/src/android/view/MotionEventTest.java
+++ b/core/tests/coretests/src/android/view/MotionEventTest.java
@@ -47,21 +47,25 @@
 public class MotionEventTest {
     private static final int ID_SOURCE_MASK = 0x3 << 30;
 
+    private PointerCoords pointerCoords(float x, float y) {
+        final var coords = new PointerCoords();
+        coords.x = x;
+        coords.y = y;
+        return coords;
+    }
+
+    private PointerProperties fingerProperties(int id) {
+        final var props = new PointerProperties();
+        props.id = id;
+        props.toolType = TOOL_TYPE_FINGER;
+        return props;
+    }
+
     @Test
     public void testObtainWithDisplayId() {
         final int pointerCount = 1;
-        PointerProperties[] properties = new PointerProperties[pointerCount];
-        final PointerCoords[] coords = new PointerCoords[pointerCount];
-        for (int i = 0; i < pointerCount; i++) {
-            final PointerCoords c = new PointerCoords();
-            c.x = i * 10;
-            c.y = i * 20;
-            coords[i] = c;
-            final PointerProperties p = new PointerProperties();
-            p.id = i;
-            p.toolType = TOOL_TYPE_FINGER;
-            properties[i] = p;
-        }
+        final var properties = new PointerProperties[]{fingerProperties(0)};
+        final var coords = new PointerCoords[]{pointerCoords(10, 20)};
 
         int displayId = 2;
         MotionEvent motionEvent = MotionEvent.obtain(0, 0, ACTION_DOWN,
@@ -125,18 +129,8 @@
     @Test
     public void testCalculatesCursorPositionForMultiTouchMouseEvents() {
         final int pointerCount = 2;
-        final PointerProperties[] properties = new PointerProperties[pointerCount];
-        final PointerCoords[] coords = new PointerCoords[pointerCount];
-
-        for (int i = 0; i < pointerCount; ++i) {
-            properties[i] = new PointerProperties();
-            properties[i].id = i;
-            properties[i].toolType = MotionEvent.TOOL_TYPE_FINGER;
-
-            coords[i] = new PointerCoords();
-            coords[i].x = 20 + i * 20;
-            coords[i].y = 60 - i * 20;
-        }
+        final var properties = new PointerProperties[]{fingerProperties(0), fingerProperties(1)};
+        final var coords = new PointerCoords[]{pointerCoords(20, 60), pointerCoords(40, 40)};
 
         final MotionEvent event = MotionEvent.obtain(0 /* downTime */,
                 0 /* eventTime */, ACTION_POINTER_DOWN, pointerCount, properties, coords,
diff --git a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java
index 180521ba7..365f348 100644
--- a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java
+++ b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java
@@ -64,9 +64,6 @@
 import android.os.Handler;
 import android.os.Message;
 import android.os.Vibrator;
-import android.platform.test.annotations.RequiresFlagsEnabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.provider.Settings;
 import android.speech.tts.TextToSpeech;
 import android.speech.tts.Voice;
@@ -74,7 +71,6 @@
 import android.view.Window;
 import android.view.WindowManager;
 import android.view.accessibility.AccessibilityManager;
-import android.view.accessibility.Flags;
 import android.view.accessibility.IAccessibilityManager;
 import android.widget.Toast;
 
@@ -87,7 +83,6 @@
 
 import org.junit.AfterClass;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -103,9 +98,6 @@
 
 @RunWith(AndroidJUnit4.class)
 public class AccessibilityShortcutControllerTest {
-    @Rule
-    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
-
     private static final String SERVICE_NAME_STRING = "fake.package/fake.service.name";
     private static final CharSequence PACKAGE_NAME_STRING = "Service name";
     private static final String SERVICE_NAME_SUMMARY = "Summary";
@@ -440,7 +432,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_UPDATE_ALWAYS_ON_A11Y_SERVICE)
     public void turnOffVolumeShortcutForAlwaysOnA11yService_shouldTurnOffA11yService()
             throws Exception {
         configureApplicationTargetSdkVersion(Build.VERSION_CODES.R);
@@ -452,7 +443,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_UPDATE_ALWAYS_ON_A11Y_SERVICE)
     public void turnOffVolumeShortcutForAlwaysOnA11yService_hasOtherTypesShortcut_shouldNotTurnOffA11yService()
             throws Exception {
         configureApplicationTargetSdkVersion(Build.VERSION_CODES.R);
diff --git a/graphics/java/Android.bp b/graphics/java/Android.bp
index ece453d..f4abd0a 100644
--- a/graphics/java/Android.bp
+++ b/graphics/java/Android.bp
@@ -11,6 +11,7 @@
 aconfig_declarations {
     name: "framework_graphics_flags",
     package: "com.android.graphics.flags",
+    container: "system",
     srcs: ["android/framework_graphics.aconfig"],
 }
 
diff --git a/graphics/java/android/framework_graphics.aconfig b/graphics/java/android/framework_graphics.aconfig
index 1e41b4d..4ab09eb 100644
--- a/graphics/java/android/framework_graphics.aconfig
+++ b/graphics/java/android/framework_graphics.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.graphics.flags"
+container: "system"
 
 flag {
      name: "exact_compute_bounds"
@@ -14,4 +15,4 @@
      namespace: "core_graphics"
      description: "Feature flag for YUV image compress to Ultra HDR."
      bug: "308978825"
-}
\ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/aconfig/Android.bp b/libs/WindowManager/Shell/aconfig/Android.bp
index 1a98ffc..7f8f57b 100644
--- a/libs/WindowManager/Shell/aconfig/Android.bp
+++ b/libs/WindowManager/Shell/aconfig/Android.bp
@@ -1,6 +1,7 @@
 aconfig_declarations {
     name: "com_android_wm_shell_flags",
     package: "com.android.wm.shell",
+    container: "system",
     srcs: [
         "multitasking.aconfig",
     ],
diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
index b61dda4..7ff204c 100644
--- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig
+++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.wm.shell"
+container: "system"
 
 flag {
     name: "enable_app_pairs"
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index a541c59..c2ba064 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -45,9 +45,6 @@
     <!-- Allow PIP to resize to a slightly bigger state upon touch/showing the menu -->
     <bool name="config_pipEnableResizeForMenu">true</bool>
 
-    <!-- PiP minimum size, which is a % based off the shorter side of display width and height -->
-    <fraction name="config_pipShortestEdgePercent">40%</fraction>
-
     <!-- Time (duration in milliseconds) that the shell waits for an app to close the PiP by itself
          if a custom action is present before closing it. -->
     <integer name="config_pipForceCloseDelay">1000</integer>
@@ -91,11 +88,45 @@
         16x16
     </string>
 
+    <!-- Default percentages for the PIP size logic.
+         1. Determine max widths
+         Subtract width of system UI and default padding from the shortest edge of the device.
+         This is the max width.
+         2. Calculate Default and Mins
+         Default is config_pipSystemPreferredDefaultSizePercent of max-width/height.
+         Min is config_pipSystemPreferredMinimumSizePercent of it. -->
+    <item name="config_pipSystemPreferredDefaultSizePercent" format="float" type="dimen">0.6</item>
+    <item name="config_pipSystemPreferredMinimumSizePercent" format="float" type="dimen">0.5</item>
+    <!-- Default percentages for the PIP size logic when the Display is close to square.
+         This is used instead when the display is square-ish, like fold-ables when unfolded,
+         to make sure that default PiP does not cover the hinge (halfway of the display).
+         0. Determine if the display is square-ish
+         If min(displayWidth, displayHeight) / max(displayWidth, displayHeight) is greater than
+         config_pipSquareDisplayThresholdForSystemPreferredSize, we use the percent for
+         square display listed below.
+         1. Determine max widths
+         Subtract width of system UI and default padding from the shortest edge of the device.
+         This is the max width.
+         2. Calculate Default and Mins
+         Default is config_pipSystemPreferredDefaultSizePercentForSquareDisplay of max-width/height.
+         Min is config_pipSystemPreferredMinimumSizePercentForSquareDisplay of it. -->
+    <item name="config_pipSquareDisplayThresholdForSystemPreferredSize"
+        format="float" type="dimen">0.95</item>
+    <item name="config_pipSystemPreferredDefaultSizePercentForSquareDisplay"
+        format="float" type="dimen">0.5</item>
+    <item name="config_pipSystemPreferredMinimumSizePercentForSquareDisplay"
+        format="float" type="dimen">0.4</item>
+
     <!-- The percentage of the screen width to use for the default width or height of
          picture-in-picture windows. Regardless of the percent set here, calculated size will never
-         be smaller than @dimen/default_minimal_size_pip_resizable_task. -->
+         be smaller than @dimen/default_minimal_size_pip_resizable_task.
+         This is used in legacy spec, use config_pipSystemPreferredDefaultSizePercent instead. -->
     <item name="config_pictureInPictureDefaultSizePercent" format="float" type="dimen">0.23</item>
 
+    <!-- PiP minimum size, which is a % based off the shorter side of display width and height.
+         This is used in legacy spec, use config_pipSystemPreferredMinimumSizePercent instead. -->
+    <fraction name="config_pipShortestEdgePercent">40%</fraction>
+
     <!-- The default aspect ratio for picture-in-picture windows. -->
     <item name="config_pictureInPictureDefaultAspectRatio" format="float" type="dimen">
         1.777778
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index fa6dd39..bf654d9 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -282,6 +282,6 @@
     <string name="expand_menu_text">Open Menu</string>
     <!-- Maximize menu maximize button string. -->
     <string name="desktop_mode_maximize_menu_maximize_text">Maximize Screen</string>
-    <!-- Maximize menu maximize button string. -->
+    <!-- Maximize menu snap buttons string. -->
     <string name="desktop_mode_maximize_menu_snap_text">Snap Screen</string>
 </resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 0867a44..313d0d2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -2335,6 +2335,11 @@
             mMainExecutor.execute(() ->
                     mController.setBubbleBarLocation(location));
         }
+
+        @Override
+        public void setBubbleBarBounds(Rect bubbleBarBounds) {
+            mMainExecutor.execute(() -> mBubblePositioner.setBubbleBarBounds(bubbleBarBounds));
+        }
     }
 
     private class BubblesImpl implements Bubbles {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
index 16134d3..c9f0f0d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
@@ -44,4 +44,6 @@
     oneway void showUserEducation(in int positionX, in int positionY) = 8;
 
     oneway void setBubbleBarLocation(in BubbleBarLocation location) = 9;
+
+    oneway void setBubbleBarBounds(in Rect bubbleBarBounds) = 10;
 }
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
index 8af4c75..45ad631 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
@@ -166,13 +166,8 @@
         bbev.setTaskViewAlpha(0f);
         bbev.setVisibility(VISIBLE);
 
-        // Set the pivot point for the scale, so the view animates out from the bubble bar.
-        Rect bubbleBarBounds = mPositioner.getBubbleBarBounds();
-        mExpandedViewContainerMatrix.setScale(
-                1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
-                1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
-                bubbleBarBounds.centerX(),
-                bubbleBarBounds.top);
+        setScaleFromBubbleBar(mExpandedViewContainerMatrix,
+                1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT);
 
         bbev.setAnimationMatrix(mExpandedViewContainerMatrix);
 
@@ -214,8 +209,8 @@
         }
         bbev.setScaleX(1f);
         bbev.setScaleY(1f);
-        mExpandedViewContainerMatrix.setScaleX(1f);
-        mExpandedViewContainerMatrix.setScaleY(1f);
+
+        setScaleFromBubbleBar(mExpandedViewContainerMatrix, 1f);
 
         PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel();
         PhysicsAnimator.getInstance(mExpandedViewContainerMatrix)
@@ -240,6 +235,16 @@
         mExpandedViewAlphaAnimator.reverse();
     }
 
+    private void setScaleFromBubbleBar(AnimatableScaleMatrix matrix, float scale) {
+        // Set the pivot point for the scale, so the view animates out from the bubble bar.
+        Rect bubbleBarBounds = mPositioner.getBubbleBarBounds();
+        matrix.setScale(
+                scale,
+                scale,
+                bubbleBarBounds.centerX(),
+                bubbleBarBounds.top);
+    }
+
     /**
      * Animate the expanded bubble when it is being dragged
      */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PhoneSizeSpecSource.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PhoneSizeSpecSource.kt
index 18c7bdd..7eb0f26 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PhoneSizeSpecSource.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PhoneSizeSpecSource.kt
@@ -18,7 +18,6 @@
 
 import android.content.Context
 import android.content.res.Resources
-import android.os.SystemProperties
 import android.util.Size
 import com.android.wm.shell.R
 import java.io.PrintWriter
@@ -36,30 +35,81 @@
     private var mOverrideMinSize: Size? = null
 
 
-    /** Default and minimum percentages for the PIP size logic.  */
-    private val mDefaultSizePercent: Float
-    private val mMinimumSizePercent: Float
+    /**
+     * Default percentages for the PIP size logic.
+     * 1. Determine max widths
+     * Subtract width of system UI and default padding from the shortest edge of the device.
+     * This is the max width.
+     * 2. Calculate Default and Mins
+     * Default is mSystemPreferredDefaultSizePercent of max-width/height.
+     * Min is mSystemPreferredMinimumSizePercent of it.
+     *
+     * NOTE: Do not use this directly, use the mPreferredDefaultSizePercent getter instead.
+     */
+    private var mSystemPreferredDefaultSizePercent = 0.6f
+    /** Minimum percentages for the PIP size logic. */
+    private var mSystemPreferredMinimumSizePercent = 0.5f
+
+    /** Threshold to categorize the Display as square, calculated as min(w, h) / max(w, h). */
+    private var mSquareDisplayThresholdForSystemPreferredSize = 0.95f
+    /**
+     * Default percentages for the PIP size logic when the Display is square-ish.
+     * This is used instead when the display is square-ish, like fold-ables when unfolded,
+     * to make sure that default PiP does not cover the hinge (halfway of the display).
+     * 1. Determine max widths
+     * Subtract width of system UI and default padding from the shortest edge of the device.
+     * This is the max width.
+     * 2. Calculate Default and Mins
+     * Default is mSystemPreferredDefaultSizePercent of max-width/height.
+     * Min is mSystemPreferredMinimumSizePercent of it.
+     *
+     * NOTE: Do not use this directly, use the mPreferredDefaultSizePercent getter instead.
+     */
+    private var mSystemPreferredDefaultSizePercentForSquareDisplay = 0.5f
+    /** Minimum percentages for the PIP size logic. */
+    private var mSystemPreferredMinimumSizePercentForSquareDisplay = 0.4f
+
+    private val mIsSquareDisplay
+        get() = minOf(pipDisplayLayoutState.displayLayout.width(),
+                        pipDisplayLayoutState.displayLayout.height()).toFloat() /
+                maxOf(pipDisplayLayoutState.displayLayout.width(),
+                        pipDisplayLayoutState.displayLayout.height()) >
+                mSquareDisplayThresholdForSystemPreferredSize
+    private val mPreferredDefaultSizePercent
+        get() = if (mIsSquareDisplay) mSystemPreferredDefaultSizePercentForSquareDisplay else
+            mSystemPreferredDefaultSizePercent
+
+    private val mPreferredMinimumSizePercent
+        get() = if (mIsSquareDisplay) mSystemPreferredMinimumSizePercentForSquareDisplay else
+            mSystemPreferredMinimumSizePercent
 
     /** Aspect ratio that the PIP size spec logic optimizes for.  */
     private var mOptimizedAspectRatio = 0f
 
     init {
-        mDefaultSizePercent = SystemProperties
-                .get("com.android.wm.shell.pip.phone.def_percentage", "0.6").toFloat()
-        mMinimumSizePercent = SystemProperties
-                .get("com.android.wm.shell.pip.phone.min_percentage", "0.5").toFloat()
-
         reloadResources()
     }
 
     private fun reloadResources() {
-        val res: Resources = context.getResources()
+        val res: Resources = context.resources
 
         mDefaultMinSize = res.getDimensionPixelSize(
                 R.dimen.default_minimal_size_pip_resizable_task)
         mOverridableMinSize = res.getDimensionPixelSize(
                 R.dimen.overridable_minimal_size_pip_resizable_task)
 
+        mSystemPreferredDefaultSizePercent = res.getFloat(
+                R.dimen.config_pipSystemPreferredDefaultSizePercent)
+        mSystemPreferredMinimumSizePercent = res.getFloat(
+                R.dimen.config_pipSystemPreferredMinimumSizePercent)
+
+        mSquareDisplayThresholdForSystemPreferredSize = res.getFloat(
+                R.dimen.config_pipSquareDisplayThresholdForSystemPreferredSize)
+        mSystemPreferredDefaultSizePercentForSquareDisplay = res.getFloat(
+                R.dimen.config_pipSystemPreferredDefaultSizePercentForSquareDisplay)
+        mSystemPreferredMinimumSizePercentForSquareDisplay = res.getFloat(
+                R.dimen.config_pipSystemPreferredMinimumSizePercentForSquareDisplay)
+
         val requestedOptAspRatio = res.getFloat(R.dimen.config_pipLargeScreenOptimizedAspectRatio)
         // make sure the optimized aspect ratio is valid with a default value to fall back to
         mOptimizedAspectRatio = if (requestedOptAspRatio > 1) {
@@ -128,7 +178,7 @@
             return minSize
         }
         val maxSize = getMaxSize(aspectRatio)
-        val defaultWidth = Math.max(Math.round(maxSize.width * mDefaultSizePercent),
+        val defaultWidth = Math.max(Math.round(maxSize.width * mPreferredDefaultSizePercent),
                 minSize.width)
         val defaultHeight = Math.round(defaultWidth / aspectRatio)
         return Size(defaultWidth, defaultHeight)
@@ -146,8 +196,8 @@
             return adjustOverrideMinSizeToAspectRatio(aspectRatio)!!
         }
         val maxSize = getMaxSize(aspectRatio)
-        var minWidth = Math.round(maxSize.width * mMinimumSizePercent)
-        var minHeight = Math.round(maxSize.height * mMinimumSizePercent)
+        var minWidth = Math.round(maxSize.width * mPreferredMinimumSizePercent)
+        var minHeight = Math.round(maxSize.height * mPreferredMinimumSizePercent)
 
         // make sure the calculated min size is not smaller than the allowed default min size
         if (aspectRatio > 1f) {
@@ -244,8 +294,8 @@
         pw.println(innerPrefix + "mOverrideMinSize=" + mOverrideMinSize)
         pw.println(innerPrefix + "mOverridableMinSize=" + mOverridableMinSize)
         pw.println(innerPrefix + "mDefaultMinSize=" + mDefaultMinSize)
-        pw.println(innerPrefix + "mDefaultSizePercent=" + mDefaultSizePercent)
-        pw.println(innerPrefix + "mMinimumSizePercent=" + mMinimumSizePercent)
+        pw.println(innerPrefix + "mDefaultSizePercent=" + mPreferredDefaultSizePercent)
+        pw.println(innerPrefix + "mMinimumSizePercent=" + mPreferredMinimumSizePercent)
         pw.println(innerPrefix + "mOptimizedAspectRatio=" + mOptimizedAspectRatio)
     }
 }
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInputConsumer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInputConsumer.java
new file mode 100644
index 0000000..03547a5
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInputConsumer.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip2.phone;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.view.BatchedInputEventReceiver;
+import android.view.Choreographer;
+import android.view.IWindowManager;
+import android.view.InputChannel;
+import android.view.InputEvent;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+
+import java.io.PrintWriter;
+
+/**
+ * Manages the input consumer that allows the Shell to directly receive input.
+ */
+public class PipInputConsumer {
+
+    private static final String TAG = PipInputConsumer.class.getSimpleName();
+
+    /**
+     * Listener interface for callers to subscribe to input events.
+     */
+    public interface InputListener {
+        /** Handles any input event. */
+        boolean onInputEvent(InputEvent ev);
+    }
+
+    /**
+     * Listener interface for callers to learn when this class is registered or unregistered with
+     * window manager
+     */
+    private interface RegistrationListener {
+        void onRegistrationChanged(boolean isRegistered);
+    }
+
+    /**
+     * Input handler used for the input consumer. Input events are batched and consumed with the
+     * SurfaceFlinger vsync.
+     */
+    private final class InputEventReceiver extends BatchedInputEventReceiver {
+
+        InputEventReceiver(InputChannel inputChannel, Looper looper,
+                Choreographer choreographer) {
+            super(inputChannel, looper, choreographer);
+        }
+
+        @Override
+        public void onInputEvent(InputEvent event) {
+            boolean handled = true;
+            try {
+                if (mListener != null) {
+                    handled = mListener.onInputEvent(event);
+                }
+            } finally {
+                finishInputEvent(event, handled);
+            }
+        }
+    }
+
+    private final IWindowManager mWindowManager;
+    private final IBinder mToken;
+    private final String mName;
+    private final ShellExecutor mMainExecutor;
+
+    private InputEventReceiver mInputEventReceiver;
+    private InputListener mListener;
+    private RegistrationListener mRegistrationListener;
+
+    /**
+     * @param name the name corresponding to the input consumer that is defined in the system.
+     */
+    public PipInputConsumer(IWindowManager windowManager, String name,
+            ShellExecutor mainExecutor) {
+        mWindowManager = windowManager;
+        mToken = new Binder();
+        mName = name;
+        mMainExecutor = mainExecutor;
+    }
+
+    /**
+     * Sets the input listener.
+     */
+    public void setInputListener(InputListener listener) {
+        mListener = listener;
+    }
+
+    /**
+     * Sets the registration listener.
+     */
+    public void setRegistrationListener(RegistrationListener listener) {
+        mRegistrationListener = listener;
+        mMainExecutor.execute(() -> {
+            if (mRegistrationListener != null) {
+                mRegistrationListener.onRegistrationChanged(mInputEventReceiver != null);
+            }
+        });
+    }
+
+    /**
+     * Check if the InputConsumer is currently registered with WindowManager
+     *
+     * @return {@code true} if registered, {@code false} if not.
+     */
+    public boolean isRegistered() {
+        return mInputEventReceiver != null;
+    }
+
+    /**
+     * Registers the input consumer.
+     */
+    public void registerInputConsumer() {
+        if (mInputEventReceiver != null) {
+            return;
+        }
+        final InputChannel inputChannel = new InputChannel();
+        try {
+            // TODO(b/113087003): Support Picture-in-picture in multi-display.
+            mWindowManager.destroyInputConsumer(mToken, DEFAULT_DISPLAY);
+            mWindowManager.createInputConsumer(mToken, mName, DEFAULT_DISPLAY, inputChannel);
+        } catch (RemoteException e) {
+            ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: Failed to create input consumer, %s", TAG, e);
+        }
+        mMainExecutor.execute(() -> {
+            mInputEventReceiver = new InputEventReceiver(inputChannel,
+                Looper.myLooper(), Choreographer.getInstance());
+            if (mRegistrationListener != null) {
+                mRegistrationListener.onRegistrationChanged(true /* isRegistered */);
+            }
+        });
+    }
+
+    /**
+     * Unregisters the input consumer.
+     */
+    public void unregisterInputConsumer() {
+        if (mInputEventReceiver == null) {
+            return;
+        }
+        try {
+            // TODO(b/113087003): Support Picture-in-picture in multi-display.
+            mWindowManager.destroyInputConsumer(mToken, DEFAULT_DISPLAY);
+        } catch (RemoteException e) {
+            ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: Failed to destroy input consumer, %s", TAG, e);
+        }
+        mInputEventReceiver.dispose();
+        mInputEventReceiver = null;
+        mMainExecutor.execute(() -> {
+            if (mRegistrationListener != null) {
+                mRegistrationListener.onRegistrationChanged(false /* isRegistered */);
+            }
+        });
+    }
+
+    /**
+     * Dumps the {@link PipInputConsumer} state.
+     */
+    public void dump(PrintWriter pw, String prefix) {
+        final String innerPrefix = prefix + "  ";
+        pw.println(prefix + TAG);
+        pw.println(innerPrefix + "registered=" + (mInputEventReceiver != null));
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java
new file mode 100644
index 0000000..04cf350
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java
@@ -0,0 +1,538 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.pip2.phone;
+
+import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_NONE;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.hardware.input.InputManager;
+import android.os.Looper;
+import android.view.BatchedInputEventReceiver;
+import android.view.Choreographer;
+import android.view.InputChannel;
+import android.view.InputEvent;
+import android.view.InputEventReceiver;
+import android.view.InputMonitor;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
+import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.common.pip.PipPerfHintController;
+import com.android.wm.shell.common.pip.PipPinchResizingAlgorithm;
+import com.android.wm.shell.common.pip.PipUiEventLogger;
+
+import java.io.PrintWriter;
+import java.util.function.Consumer;
+
+/**
+ * Helper on top of PipTouchHandler that handles inputs OUTSIDE of the PIP window, which is used to
+ * trigger dynamic resize.
+ */
+public class PipResizeGestureHandler {
+
+    private static final String TAG = "PipResizeGestureHandler";
+    private static final int PINCH_RESIZE_SNAP_DURATION = 250;
+    private static final float PINCH_RESIZE_AUTO_MAX_RATIO = 0.9f;
+
+    private final Context mContext;
+    private final PipBoundsAlgorithm mPipBoundsAlgorithm;
+    private final PipBoundsState mPipBoundsState;
+    private final PipTouchState mPipTouchState;
+    private final PhonePipMenuController mPhonePipMenuController;
+    private final PipUiEventLogger mPipUiEventLogger;
+    private final PipPinchResizingAlgorithm mPinchResizingAlgorithm;
+    private final int mDisplayId;
+    private final ShellExecutor mMainExecutor;
+
+    private final PointF mDownPoint = new PointF();
+    private final PointF mDownSecondPoint = new PointF();
+    private final PointF mLastPoint = new PointF();
+    private final PointF mLastSecondPoint = new PointF();
+    private final Point mMaxSize = new Point();
+    private final Point mMinSize = new Point();
+    private final Rect mLastResizeBounds = new Rect();
+    private final Rect mUserResizeBounds = new Rect();
+    private final Rect mDownBounds = new Rect();
+    private final Runnable mUpdateMovementBoundsRunnable;
+    private final Consumer<Rect> mUpdateResizeBoundsCallback;
+
+    private float mTouchSlop;
+
+    private boolean mAllowGesture;
+    private boolean mIsAttached;
+    private boolean mIsEnabled;
+    private boolean mEnablePinchResize;
+    private boolean mIsSysUiStateValid;
+    private boolean mThresholdCrossed;
+    private boolean mOngoingPinchToResize = false;
+    private float mAngle = 0;
+    int mFirstIndex = -1;
+    int mSecondIndex = -1;
+
+    private InputMonitor mInputMonitor;
+    private InputEventReceiver mInputEventReceiver;
+
+    @Nullable
+    private final PipPerfHintController mPipPerfHintController;
+
+    @Nullable
+    private PipPerfHintController.PipHighPerfSession mPipHighPerfSession;
+
+    private int mCtrlType;
+    private int mOhmOffset;
+
+    public PipResizeGestureHandler(Context context, PipBoundsAlgorithm pipBoundsAlgorithm,
+            PipBoundsState pipBoundsState, PipTouchState pipTouchState,
+            Runnable updateMovementBoundsRunnable,
+            PipUiEventLogger pipUiEventLogger, PhonePipMenuController menuActivityController,
+            ShellExecutor mainExecutor, @Nullable PipPerfHintController pipPerfHintController) {
+        mContext = context;
+        mDisplayId = context.getDisplayId();
+        mMainExecutor = mainExecutor;
+        mPipPerfHintController = pipPerfHintController;
+        mPipBoundsAlgorithm = pipBoundsAlgorithm;
+        mPipBoundsState = pipBoundsState;
+        mPipTouchState = pipTouchState;
+        mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable;
+        mPhonePipMenuController = menuActivityController;
+        mPipUiEventLogger = pipUiEventLogger;
+        mPinchResizingAlgorithm = new PipPinchResizingAlgorithm();
+
+        mUpdateResizeBoundsCallback = (rect) -> {
+            mUserResizeBounds.set(rect);
+            // mMotionHelper.synchronizePinnedStackBounds();
+            mUpdateMovementBoundsRunnable.run();
+            resetState();
+        };
+    }
+
+    void init() {
+        mContext.getDisplay().getRealSize(mMaxSize);
+        reloadResources();
+
+        final Resources res = mContext.getResources();
+        mEnablePinchResize = res.getBoolean(R.bool.config_pipEnablePinchResize);
+    }
+
+    void onConfigurationChanged() {
+        reloadResources();
+    }
+
+    /**
+     * Called when SysUI state changed.
+     *
+     * @param isSysUiStateValid Is SysUI valid or not.
+     */
+    public void onSystemUiStateChanged(boolean isSysUiStateValid) {
+        mIsSysUiStateValid = isSysUiStateValid;
+    }
+
+    private void reloadResources() {
+        mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
+    }
+
+    private void disposeInputChannel() {
+        if (mInputEventReceiver != null) {
+            mInputEventReceiver.dispose();
+            mInputEventReceiver = null;
+        }
+        if (mInputMonitor != null) {
+            mInputMonitor.dispose();
+            mInputMonitor = null;
+        }
+    }
+
+    void onActivityPinned() {
+        mIsAttached = true;
+        updateIsEnabled();
+    }
+
+    void onActivityUnpinned() {
+        mIsAttached = false;
+        mUserResizeBounds.setEmpty();
+        updateIsEnabled();
+    }
+
+    private void updateIsEnabled() {
+        boolean isEnabled = mIsAttached;
+        if (isEnabled == mIsEnabled) {
+            return;
+        }
+        mIsEnabled = isEnabled;
+        disposeInputChannel();
+
+        if (mIsEnabled) {
+            // Register input event receiver
+            mInputMonitor = mContext.getSystemService(InputManager.class).monitorGestureInput(
+                    "pip-resize", mDisplayId);
+            try {
+                mMainExecutor.executeBlocking(() -> {
+                    mInputEventReceiver = new PipResizeInputEventReceiver(
+                            mInputMonitor.getInputChannel(), Looper.myLooper());
+                });
+            } catch (InterruptedException e) {
+                throw new RuntimeException("Failed to create input event receiver", e);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    void onInputEvent(InputEvent ev) {
+        if (!mEnablePinchResize) {
+            // No need to handle anything if neither form of resizing is enabled.
+            return;
+        }
+
+        if (!mPipTouchState.getAllowInputEvents()) {
+            // No need to handle anything if touches are not enabled
+            return;
+        }
+
+        // Don't allow resize when PiP is stashed.
+        if (mPipBoundsState.isStashed()) {
+            return;
+        }
+
+        if (ev instanceof MotionEvent) {
+            MotionEvent mv = (MotionEvent) ev;
+            int action = mv.getActionMasked();
+            final Rect pipBounds = mPipBoundsState.getBounds();
+            if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+                if (!pipBounds.contains((int) mv.getRawX(), (int) mv.getRawY())
+                        && mPhonePipMenuController.isMenuVisible()) {
+                    mPhonePipMenuController.hideMenu();
+                }
+            }
+
+            if (mEnablePinchResize && mOngoingPinchToResize) {
+                onPinchResize(mv);
+            }
+        }
+    }
+
+    /**
+     * Checks if there is currently an on-going gesture, either drag-resize or pinch-resize.
+     */
+    public boolean hasOngoingGesture() {
+        return mCtrlType != CTRL_NONE || mOngoingPinchToResize;
+    }
+
+    public boolean isUsingPinchToZoom() {
+        return mEnablePinchResize;
+    }
+
+    public boolean isResizing() {
+        return mAllowGesture;
+    }
+
+    boolean willStartResizeGesture(MotionEvent ev) {
+        if (isInValidSysUiState()) {
+            if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
+                if (mEnablePinchResize && ev.getPointerCount() == 2) {
+                    onPinchResize(ev);
+                    mOngoingPinchToResize = mAllowGesture;
+                    return mAllowGesture;
+                }
+            }
+        }
+        return false;
+    }
+
+    private boolean isInValidSysUiState() {
+        return mIsSysUiStateValid;
+    }
+
+    private void onHighPerfSessionTimeout(PipPerfHintController.PipHighPerfSession session) {}
+
+    private void cleanUpHighPerfSessionMaybe() {
+        if (mPipHighPerfSession != null) {
+            // Close the high perf session once pointer interactions are over;
+            mPipHighPerfSession.close();
+            mPipHighPerfSession = null;
+        }
+    }
+
+    @VisibleForTesting
+    void onPinchResize(MotionEvent ev) {
+        int action = ev.getActionMasked();
+
+        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+            mFirstIndex = -1;
+            mSecondIndex = -1;
+            mAllowGesture = false;
+            finishResize();
+            cleanUpHighPerfSessionMaybe();
+        }
+
+        if (ev.getPointerCount() != 2) {
+            return;
+        }
+
+        final Rect pipBounds = mPipBoundsState.getBounds();
+        if (action == MotionEvent.ACTION_POINTER_DOWN) {
+            if (mFirstIndex == -1 && mSecondIndex == -1
+                    && pipBounds.contains((int) ev.getRawX(0), (int) ev.getRawY(0))
+                    && pipBounds.contains((int) ev.getRawX(1), (int) ev.getRawY(1))) {
+                mAllowGesture = true;
+                mFirstIndex = 0;
+                mSecondIndex = 1;
+                mDownPoint.set(ev.getRawX(mFirstIndex), ev.getRawY(mFirstIndex));
+                mDownSecondPoint.set(ev.getRawX(mSecondIndex), ev.getRawY(mSecondIndex));
+                mDownBounds.set(pipBounds);
+
+                mLastPoint.set(mDownPoint);
+                mLastSecondPoint.set(mLastSecondPoint);
+                mLastResizeBounds.set(mDownBounds);
+
+                // start the high perf session as the second pointer gets detected
+                if (mPipPerfHintController != null) {
+                    mPipHighPerfSession = mPipPerfHintController.startSession(
+                            this::onHighPerfSessionTimeout, "onPinchResize");
+                }
+            }
+        }
+
+        if (action == MotionEvent.ACTION_MOVE) {
+            if (mFirstIndex == -1 || mSecondIndex == -1) {
+                return;
+            }
+
+            float x0 = ev.getRawX(mFirstIndex);
+            float y0 = ev.getRawY(mFirstIndex);
+            float x1 = ev.getRawX(mSecondIndex);
+            float y1 = ev.getRawY(mSecondIndex);
+            mLastPoint.set(x0, y0);
+            mLastSecondPoint.set(x1, y1);
+
+            // Capture inputs
+            if (!mThresholdCrossed
+                    && (distanceBetween(mDownSecondPoint, mLastSecondPoint) > mTouchSlop
+                            || distanceBetween(mDownPoint, mLastPoint) > mTouchSlop)) {
+                pilferPointers();
+                mThresholdCrossed = true;
+                // Reset the down to begin resizing from this point
+                mDownPoint.set(mLastPoint);
+                mDownSecondPoint.set(mLastSecondPoint);
+
+                if (mPhonePipMenuController.isMenuVisible()) {
+                    mPhonePipMenuController.hideMenu();
+                }
+            }
+
+            if (mThresholdCrossed) {
+                mAngle = mPinchResizingAlgorithm.calculateBoundsAndAngle(mDownPoint,
+                        mDownSecondPoint, mLastPoint, mLastSecondPoint, mMinSize, mMaxSize,
+                        mDownBounds, mLastResizeBounds);
+
+                /*
+                mPipTaskOrganizer.scheduleUserResizePip(mDownBounds, mLastResizeBounds,
+                        mAngle, null);
+                 */
+                mPipBoundsState.setHasUserResizedPip(true);
+            }
+        }
+    }
+
+    private void snapToMovementBoundsEdge(Rect bounds, Rect movementBounds) {
+        final int leftEdge = bounds.left;
+
+
+        final int fromLeft = Math.abs(leftEdge - movementBounds.left);
+        final int fromRight = Math.abs(movementBounds.right - leftEdge);
+
+        // The PIP will be snapped to either the right or left edge, so calculate which one
+        // is closest to the current position.
+        final int newLeft = fromLeft < fromRight
+                ? movementBounds.left : movementBounds.right;
+
+        bounds.offsetTo(newLeft, mLastResizeBounds.top);
+    }
+
+    /**
+     * Resizes the pip window and updates user-resized bounds.
+     *
+     * @param bounds target bounds to resize to
+     * @param snapFraction snap fraction to apply after resizing
+     */
+    void userResizeTo(Rect bounds, float snapFraction) {
+        Rect finalBounds = new Rect(bounds);
+
+        // get the current movement bounds
+        final Rect movementBounds = mPipBoundsAlgorithm.getMovementBounds(finalBounds);
+
+        // snap the target bounds to the either left or right edge, by choosing the closer one
+        snapToMovementBoundsEdge(finalBounds, movementBounds);
+
+        // apply the requested snap fraction onto the target bounds
+        mPipBoundsAlgorithm.applySnapFraction(finalBounds, snapFraction);
+
+        // resize from current bounds to target bounds without animation
+        // mPipTaskOrganizer.scheduleUserResizePip(mPipBoundsState.getBounds(), finalBounds, null);
+        // set the flag that pip has been resized
+        mPipBoundsState.setHasUserResizedPip(true);
+
+        // finish the resize operation and update the state of the bounds
+        // mPipTaskOrganizer.scheduleFinishResizePip(finalBounds, mUpdateResizeBoundsCallback);
+    }
+
+    private void finishResize() {
+        if (!mLastResizeBounds.isEmpty()) {
+            // Pinch-to-resize needs to re-calculate snap fraction and animate to the snapped
+            // position correctly. Drag-resize does not need to move, so just finalize resize.
+            if (mOngoingPinchToResize) {
+                final Rect startBounds = new Rect(mLastResizeBounds);
+                // If user resize is pretty close to max size, just auto resize to max.
+                if (mLastResizeBounds.width() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.x
+                        || mLastResizeBounds.height() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.y) {
+                    resizeRectAboutCenter(mLastResizeBounds, mMaxSize.x, mMaxSize.y);
+                }
+
+                // If user resize is smaller than min size, auto resize to min
+                if (mLastResizeBounds.width() < mMinSize.x
+                        || mLastResizeBounds.height() < mMinSize.y) {
+                    resizeRectAboutCenter(mLastResizeBounds, mMinSize.x, mMinSize.y);
+                }
+
+                // get the current movement bounds
+                final Rect movementBounds = mPipBoundsAlgorithm
+                        .getMovementBounds(mLastResizeBounds);
+
+                // snap mLastResizeBounds to the correct edge based on movement bounds
+                snapToMovementBoundsEdge(mLastResizeBounds, movementBounds);
+
+                final float snapFraction = mPipBoundsAlgorithm.getSnapFraction(
+                        mLastResizeBounds, movementBounds);
+                mPipBoundsAlgorithm.applySnapFraction(mLastResizeBounds, snapFraction);
+
+                // disable any touch events beyond resizing too
+                mPipTouchState.setAllowInputEvents(false);
+
+                /*
+                mPipTaskOrganizer.scheduleAnimateResizePip(startBounds, mLastResizeBounds,
+                        PINCH_RESIZE_SNAP_DURATION, mAngle, mUpdateResizeBoundsCallback, () -> {
+                            // enable touch events
+                            mPipTouchState.setAllowInputEvents(true);
+                        });
+                 */
+            } else {
+                /*
+                mPipTaskOrganizer.scheduleFinishResizePip(mLastResizeBounds,
+                        TRANSITION_DIRECTION_USER_RESIZE,
+                        mUpdateResizeBoundsCallback);
+                 */
+            }
+            final float magnetRadiusPercent = (float) mLastResizeBounds.width() / mMinSize.x / 2.f;
+            mPipUiEventLogger.log(
+                    PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_RESIZE);
+        } else {
+            resetState();
+        }
+    }
+
+    private void resetState() {
+        mCtrlType = CTRL_NONE;
+        mAngle = 0;
+        mOngoingPinchToResize = false;
+        mAllowGesture = false;
+        mThresholdCrossed = false;
+    }
+
+    void setUserResizeBounds(Rect bounds) {
+        mUserResizeBounds.set(bounds);
+    }
+
+    void invalidateUserResizeBounds() {
+        mUserResizeBounds.setEmpty();
+    }
+
+    Rect getUserResizeBounds() {
+        return mUserResizeBounds;
+    }
+
+    @VisibleForTesting
+    Rect getLastResizeBounds() {
+        return mLastResizeBounds;
+    }
+
+    @VisibleForTesting
+    void pilferPointers() {
+        mInputMonitor.pilferPointers();
+    }
+
+
+    void updateMaxSize(int maxX, int maxY) {
+        mMaxSize.set(maxX, maxY);
+    }
+
+    void updateMinSize(int minX, int minY) {
+        mMinSize.set(minX, minY);
+    }
+
+    void setOhmOffset(int offset) {
+        mOhmOffset = offset;
+    }
+
+    private float distanceBetween(PointF p1, PointF p2) {
+        return (float) Math.hypot(p2.x - p1.x, p2.y - p1.y);
+    }
+
+    private void resizeRectAboutCenter(Rect rect, int w, int h) {
+        int cx = rect.centerX();
+        int cy = rect.centerY();
+        int l = cx - w / 2;
+        int r = l + w;
+        int t = cy - h / 2;
+        int b = t + h;
+        rect.set(l, t, r, b);
+    }
+
+    /**
+     * Dumps the {@link PipResizeGestureHandler} state.
+     */
+    public void dump(PrintWriter pw, String prefix) {
+        final String innerPrefix = prefix + "  ";
+        pw.println(prefix + TAG);
+        pw.println(innerPrefix + "mAllowGesture=" + mAllowGesture);
+        pw.println(innerPrefix + "mIsAttached=" + mIsAttached);
+        pw.println(innerPrefix + "mIsEnabled=" + mIsEnabled);
+        pw.println(innerPrefix + "mEnablePinchResize=" + mEnablePinchResize);
+        pw.println(innerPrefix + "mThresholdCrossed=" + mThresholdCrossed);
+        pw.println(innerPrefix + "mOhmOffset=" + mOhmOffset);
+        pw.println(innerPrefix + "mMinSize=" + mMinSize);
+        pw.println(innerPrefix + "mMaxSize=" + mMaxSize);
+    }
+
+    class PipResizeInputEventReceiver extends BatchedInputEventReceiver {
+        PipResizeInputEventReceiver(InputChannel channel, Looper looper) {
+            super(channel, looper, Choreographer.getInstance());
+        }
+
+        public void onInputEvent(InputEvent event) {
+            PipResizeGestureHandler.this.onInputEvent(event);
+            finishInputEvent(event, true);
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchGesture.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchGesture.java
new file mode 100644
index 0000000..efa5fc8
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchGesture.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip2.phone;
+
+/**
+ * A generic interface for a touch gesture.
+ */
+public abstract class PipTouchGesture {
+
+    /**
+     * Handle the touch down.
+     */
+    public void onDown(PipTouchState touchState) {}
+
+    /**
+     * Handle the touch move, and return whether the event was consumed.
+     */
+    public boolean onMove(PipTouchState touchState) {
+        return false;
+    }
+
+    /**
+     * Handle the touch up, and return whether the gesture was consumed.
+     */
+    public boolean onUp(PipTouchState touchState) {
+        return false;
+    }
+
+    /**
+     * Cleans up the high performance hint session if needed.
+     */
+    public void cleanUpHighPerfSessionMaybe() {}
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchState.java
new file mode 100644
index 0000000..d093f1e
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchState.java
@@ -0,0 +1,427 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip2.phone;
+
+import android.graphics.PointF;
+import android.view.Display;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+
+import java.io.PrintWriter;
+
+/**
+ * This keeps track of the touch state throughout the current touch gesture.
+ */
+public class PipTouchState {
+    private static final String TAG = "PipTouchState";
+    private static final boolean DEBUG = false;
+
+    @VisibleForTesting
+    public static final long DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
+    static final long HOVER_EXIT_TIMEOUT = 50;
+
+    private final ShellExecutor mMainExecutor;
+    private final ViewConfiguration mViewConfig;
+    private final Runnable mDoubleTapTimeoutCallback;
+    private final Runnable mHoverExitTimeoutCallback;
+
+    private VelocityTracker mVelocityTracker;
+    private long mDownTouchTime = 0;
+    private long mLastDownTouchTime = 0;
+    private long mUpTouchTime = 0;
+    private final PointF mDownTouch = new PointF();
+    private final PointF mDownDelta = new PointF();
+    private final PointF mLastTouch = new PointF();
+    private final PointF mLastDelta = new PointF();
+    private final PointF mVelocity = new PointF();
+    private boolean mAllowTouches = true;
+
+    // Set to false to block both PipTouchHandler and PipResizeGestureHandler's input processing
+    private boolean mAllowInputEvents = true;
+    private boolean mIsUserInteracting = false;
+    // Set to true only if the multiple taps occur within the double tap timeout
+    private boolean mIsDoubleTap = false;
+    // Set to true only if a gesture
+    private boolean mIsWaitingForDoubleTap = false;
+    private boolean mIsDragging = false;
+    // The previous gesture was a drag
+    private boolean mPreviouslyDragging = false;
+    private boolean mStartedDragging = false;
+    private boolean mAllowDraggingOffscreen = false;
+    private int mActivePointerId;
+    private int mLastTouchDisplayId = Display.INVALID_DISPLAY;
+
+    public PipTouchState(ViewConfiguration viewConfig, Runnable doubleTapTimeoutCallback,
+            Runnable hoverExitTimeoutCallback, ShellExecutor mainExecutor) {
+        mViewConfig = viewConfig;
+        mDoubleTapTimeoutCallback = doubleTapTimeoutCallback;
+        mHoverExitTimeoutCallback = hoverExitTimeoutCallback;
+        mMainExecutor = mainExecutor;
+    }
+
+    /**
+     * @return true if input processing is enabled for PiP in general.
+     */
+    public boolean getAllowInputEvents() {
+        return mAllowInputEvents;
+    }
+
+    /**
+     * @param allowInputEvents true to enable input processing for PiP in general.
+     */
+    public void setAllowInputEvents(boolean allowInputEvents) {
+        mAllowInputEvents = allowInputEvents;
+    }
+
+    /**
+     * Resets this state.
+     */
+    public void reset() {
+        mAllowDraggingOffscreen = false;
+        mIsDragging = false;
+        mStartedDragging = false;
+        mIsUserInteracting = false;
+        mLastTouchDisplayId = Display.INVALID_DISPLAY;
+    }
+
+    /**
+     * Processes a given touch event and updates the state.
+     */
+    public void onTouchEvent(MotionEvent ev) {
+        mLastTouchDisplayId = ev.getDisplayId();
+        switch (ev.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN: {
+                if (!mAllowTouches) {
+                    return;
+                }
+
+                // Initialize the velocity tracker
+                initOrResetVelocityTracker();
+                addMovementToVelocityTracker(ev);
+
+                mActivePointerId = ev.getPointerId(0);
+                if (DEBUG) {
+                    ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                            "%s: Setting active pointer id on DOWN: %d", TAG, mActivePointerId);
+                }
+                mLastTouch.set(ev.getRawX(), ev.getRawY());
+                mDownTouch.set(mLastTouch);
+                mAllowDraggingOffscreen = true;
+                mIsUserInteracting = true;
+                mDownTouchTime = ev.getEventTime();
+                mIsDoubleTap = !mPreviouslyDragging
+                        && (mDownTouchTime - mLastDownTouchTime) < DOUBLE_TAP_TIMEOUT;
+                mIsWaitingForDoubleTap = false;
+                mIsDragging = false;
+                mLastDownTouchTime = mDownTouchTime;
+                if (mDoubleTapTimeoutCallback != null) {
+                    mMainExecutor.removeCallbacks(mDoubleTapTimeoutCallback);
+                }
+                break;
+            }
+            case MotionEvent.ACTION_MOVE: {
+                // Skip event if we did not start processing this touch gesture
+                if (!mIsUserInteracting) {
+                    break;
+                }
+
+                // Update the velocity tracker
+                addMovementToVelocityTracker(ev);
+                int pointerIndex = ev.findPointerIndex(mActivePointerId);
+                if (pointerIndex == -1) {
+                    ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                            "%s: Invalid active pointer id on MOVE: %d", TAG, mActivePointerId);
+                    break;
+                }
+
+                float x = ev.getRawX(pointerIndex);
+                float y = ev.getRawY(pointerIndex);
+                mLastDelta.set(x - mLastTouch.x, y - mLastTouch.y);
+                mDownDelta.set(x - mDownTouch.x, y - mDownTouch.y);
+
+                boolean hasMovedBeyondTap = mDownDelta.length() > mViewConfig.getScaledTouchSlop();
+                if (!mIsDragging) {
+                    if (hasMovedBeyondTap) {
+                        mIsDragging = true;
+                        mStartedDragging = true;
+                    }
+                } else {
+                    mStartedDragging = false;
+                }
+                mLastTouch.set(x, y);
+                break;
+            }
+            case MotionEvent.ACTION_POINTER_UP: {
+                // Skip event if we did not start processing this touch gesture
+                if (!mIsUserInteracting) {
+                    break;
+                }
+
+                // Update the velocity tracker
+                addMovementToVelocityTracker(ev);
+
+                int pointerIndex = ev.getActionIndex();
+                int pointerId = ev.getPointerId(pointerIndex);
+                if (pointerId == mActivePointerId) {
+                    // Select a new active pointer id and reset the movement state
+                    final int newPointerIndex = (pointerIndex == 0) ? 1 : 0;
+                    mActivePointerId = ev.getPointerId(newPointerIndex);
+                    if (DEBUG) {
+                        ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                                "%s: Relinquish active pointer id on POINTER_UP: %d",
+                                TAG, mActivePointerId);
+                    }
+                    mLastTouch.set(ev.getRawX(newPointerIndex), ev.getRawY(newPointerIndex));
+                }
+                break;
+            }
+            case MotionEvent.ACTION_UP: {
+                // Skip event if we did not start processing this touch gesture
+                if (!mIsUserInteracting) {
+                    break;
+                }
+
+                // Update the velocity tracker
+                addMovementToVelocityTracker(ev);
+                mVelocityTracker.computeCurrentVelocity(1000,
+                        mViewConfig.getScaledMaximumFlingVelocity());
+                mVelocity.set(mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
+
+                int pointerIndex = ev.findPointerIndex(mActivePointerId);
+                if (pointerIndex == -1) {
+                    ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                            "%s: Invalid active pointer id on UP: %d", TAG, mActivePointerId);
+                    break;
+                }
+
+                mUpTouchTime = ev.getEventTime();
+                mLastTouch.set(ev.getRawX(pointerIndex), ev.getRawY(pointerIndex));
+                mPreviouslyDragging = mIsDragging;
+                mIsWaitingForDoubleTap = !mIsDoubleTap && !mIsDragging
+                        && (mUpTouchTime - mDownTouchTime) < DOUBLE_TAP_TIMEOUT;
+
+            }
+            // fall through to clean up
+            case MotionEvent.ACTION_CANCEL: {
+                recycleVelocityTracker();
+                break;
+            }
+            case MotionEvent.ACTION_BUTTON_PRESS: {
+                removeHoverExitTimeoutCallback();
+                break;
+            }
+        }
+    }
+
+    /**
+     * @return the velocity of the active touch pointer at the point it is lifted off the screen.
+     */
+    public PointF getVelocity() {
+        return mVelocity;
+    }
+
+    /**
+     * @return the last touch position of the active pointer.
+     */
+    public PointF getLastTouchPosition() {
+        return mLastTouch;
+    }
+
+    /**
+     * @return the movement delta between the last handled touch event and the previous touch
+     * position.
+     */
+    public PointF getLastTouchDelta() {
+        return mLastDelta;
+    }
+
+    /**
+     * @return the down touch position.
+     */
+    public PointF getDownTouchPosition() {
+        return mDownTouch;
+    }
+
+    /**
+     * @return the movement delta between the last handled touch event and the down touch
+     * position.
+     */
+    public PointF getDownTouchDelta() {
+        return mDownDelta;
+    }
+
+    /**
+     * @return whether the user has started dragging.
+     */
+    public boolean isDragging() {
+        return mIsDragging;
+    }
+
+    /**
+     * @return whether the user is currently interacting with the PiP.
+     */
+    public boolean isUserInteracting() {
+        return mIsUserInteracting;
+    }
+
+    /**
+     * @return whether the user has started dragging just in the last handled touch event.
+     */
+    public boolean startedDragging() {
+        return mStartedDragging;
+    }
+
+    /**
+     * @return Display ID of the last touch event.
+     */
+    public int getLastTouchDisplayId() {
+        return mLastTouchDisplayId;
+    }
+
+    /**
+     * Sets whether touching is currently allowed.
+     */
+    public void setAllowTouches(boolean allowTouches) {
+        mAllowTouches = allowTouches;
+
+        // If the user happens to touch down before this is sent from the system during a transition
+        // then block any additional handling by resetting the state now
+        if (mIsUserInteracting) {
+            reset();
+        }
+    }
+
+    /**
+     * Disallows dragging offscreen for the duration of the current gesture.
+     */
+    public void setDisallowDraggingOffscreen() {
+        mAllowDraggingOffscreen = false;
+    }
+
+    /**
+     * @return whether dragging offscreen is allowed during this gesture.
+     */
+    public boolean allowDraggingOffscreen() {
+        return mAllowDraggingOffscreen;
+    }
+
+    /**
+     * @return whether this gesture is a double-tap.
+     */
+    public boolean isDoubleTap() {
+        return mIsDoubleTap;
+    }
+
+    /**
+     * @return whether this gesture will potentially lead to a following double-tap.
+     */
+    public boolean isWaitingForDoubleTap() {
+        return mIsWaitingForDoubleTap;
+    }
+
+    /**
+     * Schedules the callback to run if the next double tap does not occur.  Only runs if
+     * isWaitingForDoubleTap() is true.
+     */
+    public void scheduleDoubleTapTimeoutCallback() {
+        if (mIsWaitingForDoubleTap) {
+            long delay = getDoubleTapTimeoutCallbackDelay();
+            mMainExecutor.removeCallbacks(mDoubleTapTimeoutCallback);
+            mMainExecutor.executeDelayed(mDoubleTapTimeoutCallback, delay);
+        }
+    }
+
+    long getDoubleTapTimeoutCallbackDelay() {
+        if (mIsWaitingForDoubleTap) {
+            return Math.max(0, DOUBLE_TAP_TIMEOUT - (mUpTouchTime - mDownTouchTime));
+        }
+        return -1;
+    }
+
+    /**
+     * Removes the timeout callback if it's in queue.
+     */
+    public void removeDoubleTapTimeoutCallback() {
+        mIsWaitingForDoubleTap = false;
+        mMainExecutor.removeCallbacks(mDoubleTapTimeoutCallback);
+    }
+
+    void scheduleHoverExitTimeoutCallback() {
+        mMainExecutor.removeCallbacks(mHoverExitTimeoutCallback);
+        mMainExecutor.executeDelayed(mHoverExitTimeoutCallback, HOVER_EXIT_TIMEOUT);
+    }
+
+    void removeHoverExitTimeoutCallback() {
+        mMainExecutor.removeCallbacks(mHoverExitTimeoutCallback);
+    }
+
+    void addMovementToVelocityTracker(MotionEvent event) {
+        if (mVelocityTracker == null) {
+            return;
+        }
+
+        // Add movement to velocity tracker using raw screen X and Y coordinates instead
+        // of window coordinates because the window frame may be moving at the same time.
+        float deltaX = event.getRawX() - event.getX();
+        float deltaY = event.getRawY() - event.getY();
+        event.offsetLocation(deltaX, deltaY);
+        mVelocityTracker.addMovement(event);
+        event.offsetLocation(-deltaX, -deltaY);
+    }
+
+    private void initOrResetVelocityTracker() {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        } else {
+            mVelocityTracker.clear();
+        }
+    }
+
+    private void recycleVelocityTracker() {
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+            mVelocityTracker = null;
+        }
+    }
+
+    /**
+     * Dumps the {@link PipTouchState}.
+     */
+    public void dump(PrintWriter pw, String prefix) {
+        final String innerPrefix = prefix + "  ";
+        pw.println(prefix + TAG);
+        pw.println(innerPrefix + "mAllowTouches=" + mAllowTouches);
+        pw.println(innerPrefix + "mAllowInputEvents=" + mAllowInputEvents);
+        pw.println(innerPrefix + "mActivePointerId=" + mActivePointerId);
+        pw.println(innerPrefix + "mLastTouchDisplayId=" + mLastTouchDisplayId);
+        pw.println(innerPrefix + "mDownTouch=" + mDownTouch);
+        pw.println(innerPrefix + "mDownDelta=" + mDownDelta);
+        pw.println(innerPrefix + "mLastTouch=" + mLastTouch);
+        pw.println(innerPrefix + "mLastDelta=" + mLastDelta);
+        pw.println(innerPrefix + "mVelocity=" + mVelocity);
+        pw.println(innerPrefix + "mIsUserInteracting=" + mIsUserInteracting);
+        pw.println(innerPrefix + "mIsDragging=" + mIsDragging);
+        pw.println(innerPrefix + "mStartedDragging=" + mStartedDragging);
+        pw.println(innerPrefix + "mAllowDraggingOffscreen=" + mAllowDraggingOffscreen);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index f790d2a..d0879434 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -425,7 +425,7 @@
     }
 
     boolean shouldResizeListenerHandleEvent(MotionEvent e, Point offset) {
-        return mDragResizeListener.shouldHandleEvent(e, offset);
+        return mDragResizeListener != null && mDragResizeListener.shouldHandleEvent(e, offset);
     }
 
     boolean isHandlingDragResize() {
@@ -795,7 +795,7 @@
      */
     private Region getGlobalExclusionRegion() {
         Region exclusionRegion;
-        if (mTaskInfo.isResizeable) {
+        if (mDragResizeListener != null && mTaskInfo.isResizeable) {
             exclusionRegion = mDragResizeListener.getCornersRegion();
         } else {
             exclusionRegion = new Region();
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml
index b3eb2bf..f5a8655 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml
@@ -38,8 +38,6 @@
         <!-- Increase trace size: 20mb for WM and 80mb for SF -->
         <option name="run-command" value="cmd window tracing size 20480"/>
         <option name="run-command" value="su root service call SurfaceFlinger 1029 i32 81920"/>
-        <!-- uninstall Maps, so that latest version can be installed from pStash directly -->
-        <option name="run-command" value="su root pm uninstall -k --user 0 com.google.android.apps.maps"/>
     </target_preparer>
     <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
         <option name="test-user-token" value="%TEST_USER%"/>
@@ -71,7 +69,6 @@
         <option name="install-arg" value="-g"/>
         <option name="install-arg" value="-r"/>
         <option name="test-file-name" value="pstash://com.netflix.mediaclient"/>
-        <option name="test-file-name" value="pstash://com.google.android.apps.maps"/>
     </target_preparer>
 
     <!-- Enable mocking GPS location by the test app -->
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java
index 3d5cd69..85f1da5 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java
@@ -16,33 +16,26 @@
 
 package com.android.wm.shell.pip.phone;
 
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
-
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
 import android.content.res.Resources;
-import android.os.SystemProperties;
 import android.testing.AndroidTestingRunner;
 import android.util.Size;
 import android.view.DisplayInfo;
 
-import com.android.dx.mockito.inline.extended.StaticMockitoSession;
+import com.android.wm.shell.R;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.pip.PhoneSizeSpecSource;
 import com.android.wm.shell.common.pip.PipDisplayLayoutState;
 import com.android.wm.shell.common.pip.SizeSpecSource;
 
-import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
-import org.mockito.exceptions.misusing.InvalidUseOfMatchersException;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -63,15 +56,24 @@
     private static final float DEFAULT_PERCENT = 0.6f;
     /** Minimum sizing percentage */
     private static final float MIN_PERCENT = 0.5f;
+    /** Threshold to determine if a Display is square-ish. */
+    private static final float SQUARE_DISPLAY_THRESHOLD = 0.95f;
+    /** Default sizing percentage for square-ish Display. */
+    private static final float SQUARE_DISPLAY_DEFAULT_PERCENT = 0.5f;
+    /** Minimum sizing percentage for square-ish Display. */
+    private static final float SQUARE_DISPLAY_MIN_PERCENT = 0.4f;
     /** Aspect ratio that the new PIP size spec logic optimizes for. */
     private static final float OPTIMIZED_ASPECT_RATIO = 9f / 16;
 
-    /** A map of aspect ratios to be tested to expected sizes */
-    private static Map<Float, Size> sExpectedMaxSizes;
-    private static Map<Float, Size> sExpectedDefaultSizes;
-    private static Map<Float, Size> sExpectedMinSizes;
-    /** A static mockito session object to mock {@link SystemProperties} */
-    private static StaticMockitoSession sStaticMockitoSession;
+    /** Maps of aspect ratios to be tested to expected sizes on non-square Display. */
+    private static Map<Float, Size> sNonSquareDisplayExpectedMaxSizes;
+    private static Map<Float, Size> sNonSquareDisplayExpectedDefaultSizes;
+    private static Map<Float, Size> sNonSquareDisplayExpectedMinSizes;
+
+    /** Maps of aspect ratios to be tested to expected sizes on square Display. */
+    private static Map<Float, Size> sSquareDisplayExpectedMaxSizes;
+    private static Map<Float, Size> sSquareDisplayExpectedDefaultSizes;
+    private static Map<Float, Size> sSquareDisplayExpectedMinSizes;
 
     @Mock private Context mContext;
     @Mock private Resources mResources;
@@ -80,49 +82,55 @@
     private SizeSpecSource mSizeSpecSource;
 
     /**
-     * Sets up static Mockito session for SystemProperties and mocks necessary static methods.
+     * Initializes the map with the aspect ratios to be tested and corresponding expected max sizes.
+     * This is to initialize the expectations on non-square Display only.
      */
-    private static void setUpStaticSystemPropertiesSession() {
-        sStaticMockitoSession = mockitoSession()
-                .mockStatic(SystemProperties.class).startMocking();
-        when(SystemProperties.get(anyString(), anyString())).thenAnswer(invocation -> {
-            String property = invocation.getArgument(0);
-            if (property.equals("com.android.wm.shell.pip.phone.def_percentage")) {
-                return Float.toString(DEFAULT_PERCENT);
-            } else if (property.equals("com.android.wm.shell.pip.phone.min_percentage")) {
-                return Float.toString(MIN_PERCENT);
-            }
+    private static void initNonSquareDisplayExpectedSizes() {
+        sNonSquareDisplayExpectedMaxSizes = new HashMap<>();
+        sNonSquareDisplayExpectedDefaultSizes = new HashMap<>();
+        sNonSquareDisplayExpectedMinSizes = new HashMap<>();
 
-            // throw an exception if illegal arguments are used for these tests
-            throw new InvalidUseOfMatchersException(
-                String.format("Argument %s does not match", property)
-            );
-        });
+        sNonSquareDisplayExpectedMaxSizes.put(16f / 9, new Size(1000, 563));
+        sNonSquareDisplayExpectedDefaultSizes.put(16f / 9, new Size(600, 338));
+        sNonSquareDisplayExpectedMinSizes.put(16f / 9, new Size(501, 282));
+
+        sNonSquareDisplayExpectedMaxSizes.put(4f / 3, new Size(893, 670));
+        sNonSquareDisplayExpectedDefaultSizes.put(4f / 3, new Size(536, 402));
+        sNonSquareDisplayExpectedMinSizes.put(4f / 3, new Size(447, 335));
+
+        sNonSquareDisplayExpectedMaxSizes.put(3f / 4, new Size(670, 893));
+        sNonSquareDisplayExpectedDefaultSizes.put(3f / 4, new Size(402, 536));
+        sNonSquareDisplayExpectedMinSizes.put(3f / 4, new Size(335, 447));
+
+        sNonSquareDisplayExpectedMaxSizes.put(9f / 16, new Size(563, 1001));
+        sNonSquareDisplayExpectedDefaultSizes.put(9f / 16, new Size(338, 601));
+        sNonSquareDisplayExpectedMinSizes.put(9f / 16, new Size(282, 501));
     }
 
     /**
      * Initializes the map with the aspect ratios to be tested and corresponding expected max sizes.
+     * This is to initialize the expectations on square Display only.
      */
-    private static void initExpectedSizes() {
-        sExpectedMaxSizes = new HashMap<>();
-        sExpectedDefaultSizes = new HashMap<>();
-        sExpectedMinSizes = new HashMap<>();
+    private static void initSquareDisplayExpectedSizes() {
+        sSquareDisplayExpectedMaxSizes = new HashMap<>();
+        sSquareDisplayExpectedDefaultSizes = new HashMap<>();
+        sSquareDisplayExpectedMinSizes = new HashMap<>();
 
-        sExpectedMaxSizes.put(16f / 9, new Size(1000, 563));
-        sExpectedDefaultSizes.put(16f / 9, new Size(600, 338));
-        sExpectedMinSizes.put(16f / 9, new Size(501, 282));
+        sSquareDisplayExpectedMaxSizes.put(16f / 9, new Size(1000, 563));
+        sSquareDisplayExpectedDefaultSizes.put(16f / 9, new Size(500, 281));
+        sSquareDisplayExpectedMinSizes.put(16f / 9, new Size(400, 225));
 
-        sExpectedMaxSizes.put(4f / 3, new Size(893, 670));
-        sExpectedDefaultSizes.put(4f / 3, new Size(536, 402));
-        sExpectedMinSizes.put(4f / 3, new Size(447, 335));
+        sSquareDisplayExpectedMaxSizes.put(4f / 3, new Size(893, 670));
+        sSquareDisplayExpectedDefaultSizes.put(4f / 3, new Size(447, 335));
+        sSquareDisplayExpectedMinSizes.put(4f / 3, new Size(357, 268));
 
-        sExpectedMaxSizes.put(3f / 4, new Size(670, 893));
-        sExpectedDefaultSizes.put(3f / 4, new Size(402, 536));
-        sExpectedMinSizes.put(3f / 4, new Size(335, 447));
+        sSquareDisplayExpectedMaxSizes.put(3f / 4, new Size(670, 893));
+        sSquareDisplayExpectedDefaultSizes.put(3f / 4, new Size(335, 447));
+        sSquareDisplayExpectedMinSizes.put(3f / 4, new Size(268, 357));
 
-        sExpectedMaxSizes.put(9f / 16, new Size(563, 1001));
-        sExpectedDefaultSizes.put(9f / 16, new Size(338, 601));
-        sExpectedMinSizes.put(9f / 16, new Size(282, 501));
+        sSquareDisplayExpectedMaxSizes.put(9f / 16, new Size(563, 1001));
+        sSquareDisplayExpectedDefaultSizes.put(9f / 16, new Size(282, 501));
+        sSquareDisplayExpectedMinSizes.put(9f / 16, new Size(225, 400));
     }
 
     private void forEveryTestCaseCheck(Map<Float, Size> expectedSizes,
@@ -137,20 +145,38 @@
 
     @Before
     public void setUp() {
-        initExpectedSizes();
+        initNonSquareDisplayExpectedSizes();
+        initSquareDisplayExpectedSizes();
 
-        when(mResources.getDimensionPixelSize(anyInt())).thenReturn(DEFAULT_MIN_EDGE_SIZE);
-        when(mResources.getFloat(anyInt())).thenReturn(OPTIMIZED_ASPECT_RATIO);
-        when(mResources.getString(anyInt())).thenReturn("0x0");
+        when(mResources.getFloat(R.dimen.config_pipSystemPreferredDefaultSizePercent))
+                .thenReturn(DEFAULT_PERCENT);
+        when(mResources.getFloat(R.dimen.config_pipSystemPreferredMinimumSizePercent))
+                .thenReturn(MIN_PERCENT);
+        when(mResources.getDimensionPixelSize(R.dimen.default_minimal_size_pip_resizable_task))
+                .thenReturn(DEFAULT_MIN_EDGE_SIZE);
+        when(mResources.getFloat(R.dimen.config_pipLargeScreenOptimizedAspectRatio))
+                .thenReturn(OPTIMIZED_ASPECT_RATIO);
+        when(mResources.getString(R.string.config_defaultPictureInPictureScreenEdgeInsets))
+                .thenReturn("0x0");
         when(mResources.getDisplayMetrics())
                 .thenReturn(getContext().getResources().getDisplayMetrics());
+        when(mResources.getFloat(R.dimen.config_pipSquareDisplayThresholdForSystemPreferredSize))
+                .thenReturn(SQUARE_DISPLAY_THRESHOLD);
+        when(mResources.getFloat(
+                R.dimen.config_pipSystemPreferredDefaultSizePercentForSquareDisplay))
+                .thenReturn(SQUARE_DISPLAY_DEFAULT_PERCENT);
+        when(mResources.getFloat(
+                R.dimen.config_pipSystemPreferredMinimumSizePercentForSquareDisplay))
+                .thenReturn(SQUARE_DISPLAY_MIN_PERCENT);
 
         // set up the mock context for spec handler specifically
         when(mContext.getResources()).thenReturn(mResources);
+    }
 
+    private void setupSizeSpecWithDisplayDimension(int width, int height) {
         DisplayInfo displayInfo = new DisplayInfo();
-        displayInfo.logicalWidth = DISPLAY_EDGE_SIZE;
-        displayInfo.logicalHeight = DISPLAY_EDGE_SIZE;
+        displayInfo.logicalWidth = width;
+        displayInfo.logicalHeight = height;
 
         // use the parent context (not the mocked one) to obtain the display layout
         // this is done to avoid unnecessary mocking while allowing for custom display dimensions
@@ -159,38 +185,57 @@
         mPipDisplayLayoutState = new PipDisplayLayoutState(mContext);
         mPipDisplayLayoutState.setDisplayLayout(displayLayout);
 
-        setUpStaticSystemPropertiesSession();
         mSizeSpecSource = new PhoneSizeSpecSource(mContext, mPipDisplayLayoutState);
 
         // no overridden min edge size by default
         mSizeSpecSource.setOverrideMinSize(null);
     }
 
-    @After
-    public void cleanUp() {
-        sStaticMockitoSession.finishMocking();
-    }
-
     @Test
-    public void testGetMaxSize() {
-        forEveryTestCaseCheck(sExpectedMaxSizes,
+    public void testGetMaxSize_nonSquareDisplay() {
+        setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE * 2, DISPLAY_EDGE_SIZE);
+        forEveryTestCaseCheck(sNonSquareDisplayExpectedMaxSizes,
                 (aspectRatio) -> mSizeSpecSource.getMaxSize(aspectRatio));
     }
 
     @Test
-    public void testGetDefaultSize() {
-        forEveryTestCaseCheck(sExpectedDefaultSizes,
+    public void testGetDefaultSize_nonSquareDisplay() {
+        setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE * 2, DISPLAY_EDGE_SIZE);
+        forEveryTestCaseCheck(sNonSquareDisplayExpectedDefaultSizes,
                 (aspectRatio) -> mSizeSpecSource.getDefaultSize(aspectRatio));
     }
 
     @Test
-    public void testGetMinSize() {
-        forEveryTestCaseCheck(sExpectedMinSizes,
+    public void testGetMinSize_nonSquareDisplay() {
+        setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE * 2, DISPLAY_EDGE_SIZE);
+        forEveryTestCaseCheck(sNonSquareDisplayExpectedMinSizes,
+                (aspectRatio) -> mSizeSpecSource.getMinSize(aspectRatio));
+    }
+
+    @Test
+    public void testGetMaxSize_squareDisplay() {
+        setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE, DISPLAY_EDGE_SIZE);
+        forEveryTestCaseCheck(sSquareDisplayExpectedMaxSizes,
+                (aspectRatio) -> mSizeSpecSource.getMaxSize(aspectRatio));
+    }
+
+    @Test
+    public void testGetDefaultSize_squareDisplay() {
+        setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE, DISPLAY_EDGE_SIZE);
+        forEveryTestCaseCheck(sSquareDisplayExpectedDefaultSizes,
+                (aspectRatio) -> mSizeSpecSource.getDefaultSize(aspectRatio));
+    }
+
+    @Test
+    public void testGetMinSize_squareDisplay() {
+        setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE, DISPLAY_EDGE_SIZE);
+        forEveryTestCaseCheck(sSquareDisplayExpectedMinSizes,
                 (aspectRatio) -> mSizeSpecSource.getMinSize(aspectRatio));
     }
 
     @Test
     public void testGetSizeForAspectRatio_noOverrideMinSize() {
+        setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE * 2, DISPLAY_EDGE_SIZE);
         // an initial size with 16:9 aspect ratio
         Size initSize = new Size(600, 337);
 
@@ -202,6 +247,7 @@
 
     @Test
     public void testGetSizeForAspectRatio_withOverrideMinSize() {
+        setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE * 2, DISPLAY_EDGE_SIZE);
         // an initial size with a 1:1 aspect ratio
         Size initSize = new Size(OVERRIDE_MIN_EDGE_SIZE, OVERRIDE_MIN_EDGE_SIZE);
         mSizeSpecSource.setOverrideMinSize(initSize);
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index 341c3e8c..02fa5f2 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -31,6 +31,7 @@
 aconfig_declarations {
     name: "hwui_flags",
     package: "com.android.graphics.hwui.flags",
+    container: "system",
     srcs: [
         "aconfig/hwui_flags.aconfig",
     ],
diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig
index 659bcdc..50f8b39 100644
--- a/libs/hwui/aconfig/hwui_flags.aconfig
+++ b/libs/hwui/aconfig/hwui_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.graphics.hwui.flags"
+container: "system"
 
 flag {
   name: "clip_shader"
diff --git a/media/java/android/media/FadeManagerConfiguration.java b/media/java/android/media/FadeManagerConfiguration.java
index eaafa59..6d84e70 100644
--- a/media/java/android/media/FadeManagerConfiguration.java
+++ b/media/java/android/media/FadeManagerConfiguration.java
@@ -836,7 +836,7 @@
          */
         public Builder(@NonNull FadeManagerConfiguration fmc) {
             mFadeState = fmc.mFadeState;
-            mUsageToFadeWrapperMap = fmc.mUsageToFadeWrapperMap.clone();
+            copyUsageToFadeWrapperMapInternal(fmc.mUsageToFadeWrapperMap);
             mAttrToFadeWrapperMap = new ArrayMap<AudioAttributes, FadeVolumeShaperConfigsWrapper>(
                     fmc.mAttrToFadeWrapperMap);
             mFadeableUsages = fmc.mFadeableUsages.clone();
@@ -1459,6 +1459,14 @@
             }
         }
 
+        private  void copyUsageToFadeWrapperMapInternal(
+                SparseArray<FadeVolumeShaperConfigsWrapper> usageToFadeWrapperMap) {
+            for (int index = 0; index < usageToFadeWrapperMap.size(); index++) {
+                mUsageToFadeWrapperMap.put(usageToFadeWrapperMap.keyAt(index),
+                        new FadeVolumeShaperConfigsWrapper(usageToFadeWrapperMap.valueAt(index)));
+            }
+        }
+
         private void validateFadeState(int state) {
             switch(state) {
                 case FADE_STATE_DISABLED:
@@ -1551,6 +1559,12 @@
 
         FadeVolumeShaperConfigsWrapper() {}
 
+        FadeVolumeShaperConfigsWrapper(@NonNull FadeVolumeShaperConfigsWrapper wrapper) {
+            Objects.requireNonNull(wrapper, "Fade volume shaper configs wrapper cannot be null");
+            this.mFadeOutVolShaperConfig = wrapper.mFadeOutVolShaperConfig;
+            this.mFadeInVolShaperConfig = wrapper.mFadeInVolShaperConfig;
+        }
+
         public void setFadeOutVolShaperConfig(@Nullable VolumeShaper.Configuration fadeOutConfig) {
             mFadeOutVolShaperConfig = fadeOutConfig;
         }
diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig
index c1be6b5..91c4f11 100644
--- a/media/java/android/media/flags/media_better_together.aconfig
+++ b/media/java/android/media/flags/media_better_together.aconfig
@@ -25,13 +25,6 @@
 }
 
 flag {
-    name: "disable_screen_off_broadcast_receiver"
-    namespace: "media_solutions"
-    description: "Disables the broadcast receiver that prevents scanning when the screen is off."
-    bug: "304234628"
-}
-
-flag {
     name: "fallback_to_default_handling_when_media_session_has_fixed_volume_handling"
     namespace: "media_solutions"
     description: "Fallbacks to the default handling for volume adjustment when media session has fixed volume handling and its app is in the foreground and setting a media controller."
@@ -108,6 +101,16 @@
 }
 
 flag {
+    name: "enable_mr2_service_non_main_bg_thread"
+    namespace: "media_solutions"
+    description: "Enables the use of a background thread in the media routing framework, instead of using the main thread."
+    bug: "310145678"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "enable_screen_off_scanning"
     is_exported: true
     namespace: "media_solutions"
@@ -121,3 +124,13 @@
     description: "Enables apps owning a MediaBrowserService to disconnect all connected browsers."
     bug: "185136506"
 }
+
+flag {
+    name: "enable_prevention_of_manager_scans_when_no_apps_scan"
+    namespace: "media_solutions"
+    description: "Prevents waking up route providers when no apps are scanning, even if SysUI or Settings are scanning."
+    bug: "319604673"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java
index 7ed67dc..2a0648d 100644
--- a/media/java/android/media/projection/MediaProjectionManager.java
+++ b/media/java/android/media/projection/MediaProjectionManager.java
@@ -23,9 +23,6 @@
 import android.annotation.TestApi;
 import android.app.Activity;
 import android.app.ActivityOptions.LaunchCookie;
-import android.compat.annotation.ChangeId;
-import android.compat.annotation.Disabled;
-import android.compat.annotation.Overridable;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -69,18 +66,6 @@
     private static final String TAG = "MediaProjectionManager";
 
     /**
-     * This change id ensures that users are presented with a choice of capturing a single app
-     * or the entire screen when initiating a MediaProjection session, overriding the usage of
-     * MediaProjectionConfig#createConfigForDefaultDisplay.
-     *
-     * @hide
-     */
-    @ChangeId
-    @Overridable
-    @Disabled
-    public static final long OVERRIDE_DISABLE_MEDIA_PROJECTION_SINGLE_APP_OPTION = 316897322L;
-
-    /**
      * Intent extra to customize the permission dialog based on the host app's preferences.
      * @hide
      */
diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java
index c48a956..74b5afe 100644
--- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java
+++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java
@@ -196,8 +196,7 @@
         FadeManagerConfiguration fmcObj = new FadeManagerConfiguration
                 .Builder(TEST_FADE_OUT_DURATION_MS, TEST_FADE_IN_DURATION_MS).build();
 
-        FadeManagerConfiguration fmc = new FadeManagerConfiguration
-                .Builder(fmcObj).build();
+        FadeManagerConfiguration fmc = new FadeManagerConfiguration.Builder(fmcObj).build();
 
         expect.withMessage("Fade state for copy builder").that(fmc.getFadeState())
                 .isEqualTo(fmcObj.getFadeState());
@@ -249,6 +248,45 @@
     }
 
     @Test
+    public void build_withCopyConstructor_doesnotChangeOriginal() {
+        FadeManagerConfiguration copyConstructedFmc = new FadeManagerConfiguration.Builder(mFmc)
+                .setFadeOutDurationForUsage(AudioAttributes.USAGE_MEDIA, TEST_FADE_OUT_DURATION_MS)
+                .setFadeInDurationForUsage(AudioAttributes.USAGE_MEDIA, TEST_FADE_IN_DURATION_MS)
+                .build();
+
+        expect.withMessage("Fade out duration for media usage of default constructor")
+                .that(mFmc.getFadeOutDurationForUsage(AudioAttributes.USAGE_MEDIA))
+                .isEqualTo(DEFAULT_FADE_OUT_DURATION_MS);
+        expect.withMessage("Fade out duration for media usage of default constructor")
+                .that(mFmc.getFadeInDurationForUsage(AudioAttributes.USAGE_MEDIA))
+                .isEqualTo(DEFAULT_FADE_IN_DURATION_MS);
+        expect.withMessage("Fade out duration for media usage of copy constructor")
+                .that(copyConstructedFmc.getFadeOutDurationForUsage(AudioAttributes.USAGE_MEDIA))
+                .isEqualTo(TEST_FADE_OUT_DURATION_MS);
+        expect.withMessage("Fade out duration for media usage of copy constructor")
+                .that(copyConstructedFmc.getFadeInDurationForUsage(AudioAttributes.USAGE_MEDIA))
+                .isEqualTo(TEST_FADE_IN_DURATION_MS);
+    }
+
+    @Test
+    public void build_withCopyConstructor_equals() {
+        FadeManagerConfiguration fmc = new FadeManagerConfiguration.Builder()
+                .setFadeableUsages(List.of(AudioAttributes.USAGE_MEDIA,
+                        AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE,
+                        AudioAttributes.USAGE_ASSISTANT,
+                        AudioAttributes.USAGE_EMERGENCY))
+                .setFadeOutDurationForUsage(AudioAttributes.USAGE_MEDIA, TEST_FADE_OUT_DURATION_MS)
+                .setFadeInDurationForUsage(AudioAttributes.USAGE_MEDIA, TEST_FADE_IN_DURATION_MS)
+                .build();
+
+        FadeManagerConfiguration copyConstructedFmc =
+                new FadeManagerConfiguration.Builder(fmc).build();
+
+        expect.withMessage("Fade manager config constructed using copy constructor").that(fmc)
+                .isEqualTo(copyConstructedFmc);
+    }
+
+    @Test
     public void testGetDefaultFadeOutDuration() {
         expect.withMessage("Default fade out duration")
                 .that(FadeManagerConfiguration.getDefaultFadeOutDurationMillis())
diff --git a/native/android/surface_control.cpp b/native/android/surface_control.cpp
index 9b1330f..6ce83cd 100644
--- a/native/android/surface_control.cpp
+++ b/native/android/surface_control.cpp
@@ -367,7 +367,7 @@
 
 void ASurfaceTransaction_setVisibility(ASurfaceTransaction* aSurfaceTransaction,
                                        ASurfaceControl* aSurfaceControl,
-                                       int8_t visibility) {
+                                       ASurfaceTransactionVisibility visibility) {
     CHECK_NOT_NULL(aSurfaceTransaction);
     CHECK_NOT_NULL(aSurfaceControl);
 
@@ -496,7 +496,7 @@
 
 void ASurfaceTransaction_setBufferTransparency(ASurfaceTransaction* aSurfaceTransaction,
                                                ASurfaceControl* aSurfaceControl,
-                                               int8_t transparency) {
+                                               ASurfaceTransactionTransparency transparency) {
     CHECK_NOT_NULL(aSurfaceTransaction);
     CHECK_NOT_NULL(aSurfaceControl);
 
diff --git a/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java b/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java
index a8d8f9a..6f20adf 100644
--- a/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java
+++ b/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java
@@ -39,15 +39,15 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.AtomicFile;
+import android.util.LongArrayQueue;
 import android.util.Slog;
 import android.util.Xml;
-import android.utils.BackgroundThread;
-import android.utils.LongArrayQueue;
-import android.utils.XmlUtils;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.XmlUtils;
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
 
@@ -137,17 +137,6 @@
     static final int DEFAULT_BOOT_LOOP_TRIGGER_COUNT = 5;
 
     static final long DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS = TimeUnit.MINUTES.toMillis(10);
-    // Boot loop at which packageWatchdog starts first mitigation
-    private static final String BOOT_LOOP_THRESHOLD =
-            "persist.device_config.configuration.boot_loop_threshold";
-    @VisibleForTesting
-    static final int DEFAULT_BOOT_LOOP_THRESHOLD = 15;
-    // Once boot_loop_threshold is surpassed next mitigation would be triggered after
-    // specified number of reboots.
-    private static final String BOOT_LOOP_MITIGATION_INCREMENT =
-            "persist.device_config.configuration..boot_loop_mitigation_increment";
-    @VisibleForTesting
-    static final int DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT = 2;
 
     // Threshold level at which or above user might experience significant disruption.
     private static final String MAJOR_USER_IMPACT_LEVEL_THRESHOLD =
@@ -253,15 +242,8 @@
         mConnectivityModuleConnector = connectivityModuleConnector;
         mSystemClock = clock;
         mNumberOfNativeCrashPollsRemaining = NUMBER_OF_NATIVE_CRASH_POLLS;
-        if (Flags.recoverabilityDetection()) {
-            mBootThreshold = new BootThreshold(DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
-                    DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS,
-                    SystemProperties.getInt(BOOT_LOOP_MITIGATION_INCREMENT,
-                            DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT));
-        } else {
-            mBootThreshold = new BootThreshold(DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
-                    DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS);
-        }
+        mBootThreshold = new BootThreshold(DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
+                DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS);
 
         loadFromFile();
         sPackageWatchdog = this;
@@ -526,10 +508,16 @@
     /**
      * Called when the system server boots. If the system server is detected to be in a boot loop,
      * query each observer and perform the mitigation action with the lowest user impact.
+     *
+     * Note: PackageWatchdog considers system_server restart loop as bootloop. Full reboots
+     * are not counted in bootloop.
      */
     @SuppressWarnings("GuardedBy")
     public void noteBoot() {
         synchronized (mLock) {
+            // if boot count has reached threshold, start mitigation.
+            // We wait until threshold number of restarts only for the first time. Perform
+            // mitigations for every restart after that.
             boolean mitigate = mBootThreshold.incrementAndTest();
             if (mitigate) {
                 if (!Flags.recoverabilityDetection()) {
@@ -557,17 +545,13 @@
                 }
                 if (currentObserverToNotify != null) {
                     if (Flags.recoverabilityDetection()) {
-                        if (currentObserverImpact < getUserImpactLevelLimit()
-                                || (currentObserverImpact >= getUserImpactLevelLimit()
-                                        && mBootThreshold.getCount() >= getBootLoopThreshold())) {
-                            int currentObserverMitigationCount =
-                                    currentObserverInternal.getBootMitigationCount() + 1;
-                            currentObserverInternal.setBootMitigationCount(
-                                    currentObserverMitigationCount);
-                            saveAllObserversBootMitigationCountToMetadata(METADATA_FILE);
-                            currentObserverToNotify.executeBootLoopMitigation(
-                                    currentObserverMitigationCount);
-                        }
+                        int currentObserverMitigationCount =
+                                currentObserverInternal.getBootMitigationCount() + 1;
+                        currentObserverInternal.setBootMitigationCount(
+                                currentObserverMitigationCount);
+                        saveAllObserversBootMitigationCountToMetadata(METADATA_FILE);
+                        currentObserverToNotify.executeBootLoopMitigation(
+                                currentObserverMitigationCount);
                     } else {
                         mBootThreshold.setMitigationCount(mitigationCount);
                         mBootThreshold.saveMitigationCountToMetadata();
@@ -647,11 +631,6 @@
                 DEFAULT_MAJOR_USER_IMPACT_LEVEL_THRESHOLD);
     }
 
-    private int getBootLoopThreshold() {
-        return SystemProperties.getInt(BOOT_LOOP_THRESHOLD,
-                DEFAULT_BOOT_LOOP_THRESHOLD);
-    }
-
     /** Possible severity values of the user impact of a {@link PackageHealthObserver#execute}. */
     @Retention(SOURCE)
     @IntDef(value = {PackageHealthObserverImpact.USER_IMPACT_LEVEL_0,
@@ -1827,16 +1806,10 @@
 
         private final int mBootTriggerCount;
         private final long mTriggerWindow;
-        private final int mBootMitigationIncrement;
 
         BootThreshold(int bootTriggerCount, long triggerWindow) {
-            this(bootTriggerCount, triggerWindow, /*bootMitigationIncrement=*/ 1);
-        }
-
-        BootThreshold(int bootTriggerCount, long triggerWindow, int bootMitigationIncrement) {
             this.mBootTriggerCount = bootTriggerCount;
             this.mTriggerWindow = triggerWindow;
-            this.mBootMitigationIncrement = bootMitigationIncrement;
         }
 
         public void reset() {
@@ -1915,6 +1888,7 @@
             } else {
                 readMitigationCountFromMetadataIfNecessary();
             }
+
             final long now = mSystemClock.uptimeMillis();
             if (now - getStart() < 0) {
                 Slog.e(TAG, "Window was less than zero. Resetting start to current time.");
@@ -1939,20 +1913,32 @@
                 setCount(count);
                 EventLogTags.writeRescueNote(Process.ROOT_UID, count, window);
                 if (Flags.recoverabilityDetection()) {
-                    boolean mitigate = (count >= mBootTriggerCount)
-                            && (count - mBootTriggerCount) % mBootMitigationIncrement == 0;
-                    return mitigate;
+                    // After a reboot (e.g. by WARM_REBOOT or mainline rollback) we apply
+                    // mitigations without waiting for DEFAULT_BOOT_LOOP_TRIGGER_COUNT.
+                    return (count >= mBootTriggerCount)
+                            || (performedMitigationsDuringWindow() && count > 1);
                 }
                 return count >= mBootTriggerCount;
             }
         }
 
         @GuardedBy("mLock")
+        private boolean performedMitigationsDuringWindow() {
+            for (ObserverInternal observerInternal: mAllObservers.values()) {
+                if (observerInternal.getBootMitigationCount() > 0) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @GuardedBy("mLock")
         private void resetAllObserversBootMitigationCount() {
             for (int i = 0; i < mAllObservers.size(); i++) {
                 final ObserverInternal observer = mAllObservers.valueAt(i);
                 observer.setBootMitigationCount(0);
             }
+            saveAllObserversBootMitigationCountToMetadata(METADATA_FILE);
         }
 
         @GuardedBy("mLock")
diff --git a/packages/CrashRecovery/services/java/com/android/server/RescueParty.java b/packages/CrashRecovery/services/java/com/android/server/RescueParty.java
index 7093ba4..271d552 100644
--- a/packages/CrashRecovery/services/java/com/android/server/RescueParty.java
+++ b/packages/CrashRecovery/services/java/com/android/server/RescueParty.java
@@ -31,6 +31,7 @@
 import android.crashrecovery.flags.Flags;
 import android.os.Build;
 import android.os.Environment;
+import android.os.FileUtils;
 import android.os.PowerManager;
 import android.os.RecoverySystem;
 import android.os.SystemClock;
@@ -43,11 +44,10 @@
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.Slog;
-import android.utils.ArrayUtils;
-import android.utils.FileUtils;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
 import com.android.server.PackageWatchdog.FailureReasons;
 import com.android.server.PackageWatchdog.PackageHealthObserver;
 import com.android.server.PackageWatchdog.PackageHealthObserverImpact;
@@ -139,7 +139,7 @@
     static final String NAMESPACE_TO_PACKAGE_MAPPING_FLAG =
             "namespace_to_package_mapping";
     @VisibleForTesting
-    static final long DEFAULT_FACTORY_RESET_THROTTLE_DURATION_MIN = 10;
+    static final long DEFAULT_FACTORY_RESET_THROTTLE_DURATION_MIN = 1440;
 
     private static final String NAME = "rescue-party-observer";
 
diff --git a/packages/CrashRecovery/services/java/com/android/utils/ArrayUtils.java b/packages/CrashRecovery/services/java/com/android/utils/ArrayUtils.java
deleted file mode 100644
index fa4d6af..0000000
--- a/packages/CrashRecovery/services/java/com/android/utils/ArrayUtils.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.utils;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-
-import java.io.File;
-import java.util.List;
-import java.util.Objects;
-
-/**
- * Copied over from frameworks/base/core/java/com/android/internal/util/ArrayUtils.java
- *
- * @hide
- */
-public class ArrayUtils {
-    private ArrayUtils() { /* cannot be instantiated */ }
-    public static final File[] EMPTY_FILE = new File[0];
-
-
-    /**
-     * Return first index of {@code value} in {@code array}, or {@code -1} if
-     * not found.
-     */
-    public static <T> int indexOf(@Nullable T[] array, T value) {
-        if (array == null) return -1;
-        for (int i = 0; i < array.length; i++) {
-            if (Objects.equals(array[i], value)) return i;
-        }
-        return -1;
-    }
-
-    /** @hide */
-    public static @NonNull File[] defeatNullable(@Nullable File[] val) {
-        return (val != null) ? val : EMPTY_FILE;
-    }
-
-    /**
-     * Checks if given array is null or has zero elements.
-     */
-    public static boolean isEmpty(@Nullable int[] array) {
-        return array == null || array.length == 0;
-    }
-
-    /**
-     * True if the byte array is null or has length 0.
-     */
-    public static boolean isEmpty(@Nullable byte[] array) {
-        return array == null || array.length == 0;
-    }
-
-    /**
-     * Converts from List of bytes to byte array
-     * @param list
-     * @return byte[]
-     */
-    public static byte[] toPrimitive(List<byte[]> list) {
-        if (list.size() == 0) {
-            return new byte[0];
-        }
-        int byteLen = list.get(0).length;
-        byte[] array = new byte[list.size() * byteLen];
-        for (int i = 0; i < list.size(); i++) {
-            for (int j = 0; j < list.get(i).length; j++) {
-                array[i * byteLen + j] = list.get(i)[j];
-            }
-        }
-        return array;
-    }
-
-    /**
-     * Adds value to given array if not already present, providing set-like
-     * behavior.
-     */
-    public static @NonNull int[] appendInt(@Nullable int[] cur, int val) {
-        return appendInt(cur, val, false);
-    }
-
-    /**
-     * Adds value to given array.
-     */
-    public static @NonNull int[] appendInt(@Nullable int[] cur, int val,
-            boolean allowDuplicates) {
-        if (cur == null) {
-            return new int[] { val };
-        }
-        final int n = cur.length;
-        if (!allowDuplicates) {
-            for (int i = 0; i < n; i++) {
-                if (cur[i] == val) {
-                    return cur;
-                }
-            }
-        }
-        int[] ret = new int[n + 1];
-        System.arraycopy(cur, 0, ret, 0, n);
-        ret[n] = val;
-        return ret;
-    }
-}
diff --git a/packages/CrashRecovery/services/java/com/android/utils/BackgroundThread.java b/packages/CrashRecovery/services/java/com/android/utils/BackgroundThread.java
deleted file mode 100644
index afcf689..0000000
--- a/packages/CrashRecovery/services/java/com/android/utils/BackgroundThread.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- *  * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.utils;
-
-import android.annotation.NonNull;
-import android.os.Handler;
-import android.os.HandlerThread;
-
-import com.android.internal.annotations.GuardedBy;
-
-import java.util.concurrent.Executor;
-
-/**
- * Thread for asynchronous event processing. This thread is configured as
- * {@link android.os.Process#THREAD_PRIORITY_BACKGROUND}, which means fewer CPU
- * resources will be dedicated to it, and it will "have less chance of impacting
- * the responsiveness of the user interface."
- * <p>
- * This thread is best suited for tasks that the user is not actively waiting
- * for, or for tasks that the user expects to be executed eventually.
- *
- * @see com.android.internal.os.BackgroundThread
- *
- * TODO: b/326916057 depend on modules-utils-backgroundthread instead
- * @hide
- */
-public final class BackgroundThread extends HandlerThread {
-    private static final Object sLock = new Object();
-
-    @GuardedBy("sLock")
-    private static BackgroundThread sInstance;
-    @GuardedBy("sLock")
-    private static Handler sHandler;
-    @GuardedBy("sLock")
-    private static HandlerExecutor sHandlerExecutor;
-
-    private BackgroundThread() {
-        super(BackgroundThread.class.getName(), android.os.Process.THREAD_PRIORITY_BACKGROUND);
-    }
-
-    @GuardedBy("sLock")
-    private static void ensureThreadLocked() {
-        if (sInstance == null) {
-            sInstance = new BackgroundThread();
-            sInstance.start();
-            sHandler = new Handler(sInstance.getLooper());
-            sHandlerExecutor = new HandlerExecutor(sHandler);
-        }
-    }
-
-    /**
-     * Get the singleton instance of this class.
-     *
-     * @return the singleton instance of this class
-     */
-    @NonNull
-    public static BackgroundThread get() {
-        synchronized (sLock) {
-            ensureThreadLocked();
-            return sInstance;
-        }
-    }
-
-    /**
-     * Get the singleton {@link Handler} for this class.
-     *
-     * @return the singleton {@link Handler} for this class.
-     */
-    @NonNull
-    public static Handler getHandler() {
-        synchronized (sLock) {
-            ensureThreadLocked();
-            return sHandler;
-        }
-    }
-
-    /**
-     * Get the singleton {@link Executor} for this class.
-     *
-     * @return the singleton {@link Executor} for this class.
-     */
-    @NonNull
-    public static Executor getExecutor() {
-        synchronized (sLock) {
-            ensureThreadLocked();
-            return sHandlerExecutor;
-        }
-    }
-}
diff --git a/packages/CrashRecovery/services/java/com/android/utils/FileUtils.java b/packages/CrashRecovery/services/java/com/android/utils/FileUtils.java
deleted file mode 100644
index e4923bf..0000000
--- a/packages/CrashRecovery/services/java/com/android/utils/FileUtils.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.utils;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-
-import java.io.BufferedInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * Bits and pieces copied from hidden API of android.os.FileUtils.
- *
- * @hide
- */
-public class FileUtils {
-    /**
-     * Read a text file into a String, optionally limiting the length.
-     *
-     * @param file     to read (will not seek, so things like /proc files are OK)
-     * @param max      length (positive for head, negative of tail, 0 for no limit)
-     * @param ellipsis to add of the file was truncated (can be null)
-     * @return the contents of the file, possibly truncated
-     * @throws IOException if something goes wrong reading the file
-     * @hide
-     */
-    public static @Nullable String readTextFile(@Nullable File file, @Nullable int max,
-            @Nullable String ellipsis) throws IOException {
-        InputStream input = new FileInputStream(file);
-        // wrapping a BufferedInputStream around it because when reading /proc with unbuffered
-        // input stream, bytes read not equal to buffer size is not necessarily the correct
-        // indication for EOF; but it is true for BufferedInputStream due to its implementation.
-        BufferedInputStream bis = new BufferedInputStream(input);
-        try {
-            long size = file.length();
-            if (max > 0 || (size > 0 && max == 0)) {  // "head" mode: read the first N bytes
-                if (size > 0 && (max == 0 || size < max)) max = (int) size;
-                byte[] data = new byte[max + 1];
-                int length = bis.read(data);
-                if (length <= 0) return "";
-                if (length <= max) return new String(data, 0, length);
-                if (ellipsis == null) return new String(data, 0, max);
-                return new String(data, 0, max) + ellipsis;
-            } else if (max < 0) {  // "tail" mode: keep the last N
-                int len;
-                boolean rolled = false;
-                byte[] last = null;
-                byte[] data = null;
-                do {
-                    if (last != null) rolled = true;
-                    byte[] tmp = last;
-                    last = data;
-                    data = tmp;
-                    if (data == null) data = new byte[-max];
-                    len = bis.read(data);
-                } while (len == data.length);
-
-                if (last == null && len <= 0) return "";
-                if (last == null) return new String(data, 0, len);
-                if (len > 0) {
-                    rolled = true;
-                    System.arraycopy(last, len, last, 0, last.length - len);
-                    System.arraycopy(data, 0, last, last.length - len, len);
-                }
-                if (ellipsis == null || !rolled) return new String(last);
-                return ellipsis + new String(last);
-            } else {  // "cat" mode: size unknown, read it all in streaming fashion
-                ByteArrayOutputStream contents = new ByteArrayOutputStream();
-                int len;
-                byte[] data = new byte[1024];
-                do {
-                    len = bis.read(data);
-                    if (len > 0) contents.write(data, 0, len);
-                } while (len == data.length);
-                return contents.toString();
-            }
-        } finally {
-            bis.close();
-            input.close();
-        }
-    }
-
-    /**
-     * Perform an fsync on the given FileOutputStream. The stream at this
-     * point must be flushed but not yet closed.
-     *
-     * @hide
-     */
-    public static boolean sync(FileOutputStream stream) {
-        try {
-            if (stream != null) {
-                stream.getFD().sync();
-            }
-            return true;
-        } catch (IOException e) {
-        }
-        return false;
-    }
-
-    /**
-     * List the files in the directory or return empty file.
-     *
-     * @hide
-     */
-    public static @NonNull File[] listFilesOrEmpty(@Nullable File dir) {
-        return (dir != null) ? ArrayUtils.defeatNullable(dir.listFiles())
-            : ArrayUtils.EMPTY_FILE;
-    }
-}
diff --git a/packages/CrashRecovery/services/java/com/android/utils/HandlerExecutor.java b/packages/CrashRecovery/services/java/com/android/utils/HandlerExecutor.java
deleted file mode 100644
index fdb15e2..0000000
--- a/packages/CrashRecovery/services/java/com/android/utils/HandlerExecutor.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.utils;
-
-import android.annotation.NonNull;
-import android.os.Handler;
-
-import java.util.Objects;
-import java.util.concurrent.Executor;
-import java.util.concurrent.RejectedExecutionException;
-
-/**
- * An adapter {@link Executor} that posts all executed tasks onto the given
- * {@link Handler}.
- *
- * TODO: b/326916057 depend on modules-utils-backgroundthread instead
- * @hide
- */
-public class HandlerExecutor implements Executor {
-    private final Handler mHandler;
-
-    public HandlerExecutor(@NonNull Handler handler) {
-        mHandler = Objects.requireNonNull(handler);
-    }
-
-    @Override
-    public void execute(Runnable command) {
-        if (!mHandler.post(command)) {
-            throw new RejectedExecutionException(mHandler + " is shutting down");
-        }
-    }
-}
diff --git a/packages/CrashRecovery/services/java/com/android/utils/LongArrayQueue.java b/packages/CrashRecovery/services/java/com/android/utils/LongArrayQueue.java
deleted file mode 100644
index 5cdc253..0000000
--- a/packages/CrashRecovery/services/java/com/android/utils/LongArrayQueue.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.utils;
-
-import libcore.util.EmptyArray;
-
-import java.util.NoSuchElementException;
-
-/**
- * Copied from frameworks/base/core/java/android/util/LongArrayQueue.java
- *
- * @hide
- */
-public class LongArrayQueue {
-
-    private long[] mValues;
-    private int mSize;
-    private int mHead;
-    private int mTail;
-
-    private long[] newUnpaddedLongArray(int num) {
-        return new long[num];
-    }
-    /**
-     * Initializes a queue with the given starting capacity.
-     *
-     * @param initialCapacity the capacity.
-     */
-    public LongArrayQueue(int initialCapacity) {
-        if (initialCapacity == 0) {
-            mValues = EmptyArray.LONG;
-        } else {
-            mValues = newUnpaddedLongArray(initialCapacity);
-        }
-        mSize = 0;
-        mHead = mTail = 0;
-    }
-
-    /**
-     * Initializes a queue with default starting capacity.
-     */
-    public LongArrayQueue() {
-        this(16);
-    }
-
-    /** @hide */
-    public static int growSize(int currentSize) {
-        return currentSize <= 4 ? 8 : currentSize * 2;
-    }
-
-    private void grow() {
-        if (mSize < mValues.length) {
-            throw new IllegalStateException("Queue not full yet!");
-        }
-        final int newSize = growSize(mSize);
-        final long[] newArray = newUnpaddedLongArray(newSize);
-        final int r = mValues.length - mHead; // Number of elements on and to the right of head.
-        System.arraycopy(mValues, mHead, newArray, 0, r);
-        System.arraycopy(mValues, 0, newArray, r, mHead);
-        mValues = newArray;
-        mHead = 0;
-        mTail = mSize;
-    }
-
-    /**
-     * Returns the number of elements in the queue.
-     */
-    public int size() {
-        return mSize;
-    }
-
-    /**
-     * Removes all elements from this queue.
-     */
-    public void clear() {
-        mSize = 0;
-        mHead = mTail = 0;
-    }
-
-    /**
-     * Adds a value to the tail of the queue.
-     *
-     * @param value the value to be added.
-     */
-    public void addLast(long value) {
-        if (mSize == mValues.length) {
-            grow();
-        }
-        mValues[mTail] = value;
-        mTail = (mTail + 1) % mValues.length;
-        mSize++;
-    }
-
-    /**
-     * Removes an element from the head of the queue.
-     *
-     * @return the element at the head of the queue.
-     * @throws NoSuchElementException if the queue is empty.
-     */
-    public long removeFirst() {
-        if (mSize == 0) {
-            throw new NoSuchElementException("Queue is empty!");
-        }
-        final long ret = mValues[mHead];
-        mHead = (mHead + 1) % mValues.length;
-        mSize--;
-        return ret;
-    }
-
-    /**
-     * Returns the element at the given position from the head of the queue, where 0 represents the
-     * head of the queue.
-     *
-     * @param position the position from the head of the queue.
-     * @return the element found at the given position.
-     * @throws IndexOutOfBoundsException if {@code position} < {@code 0} or
-     *                                   {@code position} >= {@link #size()}
-     */
-    public long get(int position) {
-        if (position < 0 || position >= mSize) {
-            throw new IndexOutOfBoundsException("Index " + position
-                + " not valid for a queue of size " + mSize);
-        }
-        final int index = (mHead + position) % mValues.length;
-        return mValues[index];
-    }
-
-    /**
-     * Returns the element at the head of the queue, without removing it.
-     *
-     * @return the element at the head of the queue.
-     * @throws NoSuchElementException if the queue is empty
-     */
-    public long peekFirst() {
-        if (mSize == 0) {
-            throw new NoSuchElementException("Queue is empty!");
-        }
-        return mValues[mHead];
-    }
-
-    /**
-     * Returns the element at the tail of the queue.
-     *
-     * @return the element at the tail of the queue.
-     * @throws NoSuchElementException if the queue is empty.
-     */
-    public long peekLast() {
-        if (mSize == 0) {
-            throw new NoSuchElementException("Queue is empty!");
-        }
-        final int index = (mTail == 0) ? mValues.length - 1 : mTail - 1;
-        return mValues[index];
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public String toString() {
-        if (mSize <= 0) {
-            return "{}";
-        }
-
-        final StringBuilder buffer = new StringBuilder(mSize * 64);
-        buffer.append('{');
-        buffer.append(get(0));
-        for (int i = 1; i < mSize; i++) {
-            buffer.append(", ");
-            buffer.append(get(i));
-        }
-        buffer.append('}');
-        return buffer.toString();
-    }
-}
diff --git a/packages/CrashRecovery/services/java/com/android/utils/XmlUtils.java b/packages/CrashRecovery/services/java/com/android/utils/XmlUtils.java
deleted file mode 100644
index dbbef61..0000000
--- a/packages/CrashRecovery/services/java/com/android/utils/XmlUtils.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.utils;
-
-import android.annotation.NonNull;
-import android.system.ErrnoException;
-import android.system.Os;
-
-import com.android.modules.utils.TypedXmlPullParser;
-
-import libcore.util.XmlObjectFactory;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
-import java.io.BufferedInputStream;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * Copied over partly from frameworks/base/core/java/com/android/internal/util/XmlUtils.java
- *
- * @hide
- */
-public class XmlUtils {
-
-    private static final String STRING_ARRAY_SEPARATOR = ":";
-
-    /** @hide */
-    public static final void beginDocument(XmlPullParser parser, String firstElementName)
-            throws XmlPullParserException, IOException {
-        int type;
-        while ((type = parser.next()) != parser.START_TAG
-            && type != parser.END_DOCUMENT) {
-            // Do nothing
-        }
-
-        if (type != parser.START_TAG) {
-            throw new XmlPullParserException("No start tag found");
-        }
-
-        if (!parser.getName().equals(firstElementName)) {
-            throw new XmlPullParserException("Unexpected start tag: found " + parser.getName()
-                + ", expected " + firstElementName);
-        }
-    }
-
-    /** @hide */
-    public static boolean nextElementWithin(XmlPullParser parser, int outerDepth)
-            throws IOException, XmlPullParserException {
-        for (;;) {
-            int type = parser.next();
-            if (type == XmlPullParser.END_DOCUMENT
-                    || (type == XmlPullParser.END_TAG && parser.getDepth() == outerDepth)) {
-                return false;
-            }
-            if (type == XmlPullParser.START_TAG
-                    && parser.getDepth() == outerDepth + 1) {
-                return true;
-            }
-        }
-    }
-
-    private static XmlPullParser newPullParser() {
-        try {
-            XmlPullParser parser = XmlObjectFactory.newXmlPullParser();
-            parser.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, true);
-            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
-            return parser;
-        } catch (XmlPullParserException e) {
-            throw new AssertionError();
-        }
-    }
-
-    /** @hide */
-    public static @NonNull TypedXmlPullParser resolvePullParser(@NonNull InputStream in)
-            throws IOException {
-        final byte[] magic = new byte[4];
-        if (in instanceof FileInputStream) {
-            try {
-                Os.pread(((FileInputStream) in).getFD(), magic, 0, magic.length, 0);
-            } catch (ErrnoException e) {
-                throw e.rethrowAsIOException();
-            }
-        } else {
-            if (!in.markSupported()) {
-                in = new BufferedInputStream(in);
-            }
-            in.mark(8);
-            in.read(magic);
-            in.reset();
-        }
-
-        final TypedXmlPullParser xml;
-        xml = (TypedXmlPullParser) newPullParser();
-        try {
-            xml.setInput(in, "UTF_8");
-        } catch (XmlPullParserException e) {
-            throw new IOException(e);
-        }
-        return xml;
-    }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt
index d21077e..a30956e 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt
@@ -98,7 +98,8 @@
     context: Context,
     openMoreOptionsPage: () -> Unit,
     sendDataToProvider: (EntryInfo, BiometricPrompt.AuthenticationResult) -> Unit,
-    onCancelFlowAndFinish: (String) -> Unit,
+    onCancelFlowAndFinish: () -> Unit,
+    onIllegalStateAndFinish: (String) -> Unit,
     getRequestDisplayInfo: RequestDisplayInfo? = null,
     getProviderInfoList: List<ProviderInfo>? = null,
     getProviderDisplayInfo: ProviderDisplayInfo? = null,
@@ -131,7 +132,7 @@
 
     val callback: BiometricPrompt.AuthenticationCallback =
         setupBiometricAuthenticationCallback(sendDataToProvider, biometricEntry,
-            onCancelFlowAndFinish)
+            onCancelFlowAndFinish, onIllegalStateAndFinish)
 
     val cancellationSignal = CancellationSignal()
     cancellationSignal.setOnCancelListener {
@@ -211,7 +212,8 @@
 private fun setupBiometricAuthenticationCallback(
     sendDataToProvider: (EntryInfo, BiometricPrompt.AuthenticationResult) -> Unit,
     selectedEntry: EntryInfo,
-    onCancelFlowAndFinish: (String) -> Unit
+    onCancelFlowAndFinish: () -> Unit,
+    onIllegalStateAndFinish: (String) -> Unit,
 ): BiometricPrompt.AuthenticationCallback {
     val callback: BiometricPrompt.AuthenticationCallback =
         object : BiometricPrompt.AuthenticationCallback() {
@@ -224,14 +226,12 @@
                     if (authResult != null) {
                         sendDataToProvider(selectedEntry, authResult)
                     } else {
-                        onCancelFlowAndFinish("The biometric flow succeeded but unexpectedly " +
+                        onIllegalStateAndFinish("The biometric flow succeeded but unexpectedly " +
                                 "returned a null value.")
-                        // TODO(b/326243754) : Propagate to provider
                     }
                 } catch (e: Exception) {
-                    onCancelFlowAndFinish("The biometric flow succeeded but failed on handling " +
+                    onIllegalStateAndFinish("The biometric flow succeeded but failed on handling " +
                             "the result. See: \n$e\n")
-                    // TODO(b/326243754) : Propagate to provider
                 }
             }
 
@@ -245,6 +245,12 @@
             override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
                 super.onAuthenticationError(errorCode, errString)
                 Log.d(TAG, "Authentication error-ed out: $errorCode and $errString")
+                if (errorCode == BiometricPrompt.BIOMETRIC_ERROR_USER_CANCELED) {
+                    // Note that because the biometric prompt is imbued directly
+                    // into the selector, parity applies to the selector's cancellation instead
+                    // of the provider's biometric prompt cancellation.
+                    onCancelFlowAndFinish()
+                }
                 // TODO(b/326243754) : Propagate to provider
             }
 
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
index b59ccc2..4d7272c 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
@@ -148,7 +148,8 @@
                                 // activeEntry will always be what represents the single tap flow
                                 biometricEntry = getCredentialUiState.activeEntry,
                                 onMoreOptionSelected = viewModel::getFlowOnMoreOptionSelected,
-                                onCancelFlowAndFinish = viewModel::onIllegalUiState,
+                                onCancelFlowAndFinish = viewModel::onUserCancel,
+                                onIllegalStateAndFinish = viewModel::onIllegalUiState,
                                 requestDisplayInfo = getCredentialUiState.requestDisplayInfo,
                                 providerInfoList = getCredentialUiState.providerInfoList,
                                 providerDisplayInfo = getCredentialUiState.providerDisplayInfo,
@@ -212,7 +213,8 @@
 @Composable
 internal fun BiometricSelectionPage(
     biometricEntry: EntryInfo?,
-    onCancelFlowAndFinish: (String) -> Unit,
+    onCancelFlowAndFinish: () -> Unit,
+    onIllegalStateAndFinish: (String) -> Unit,
     onMoreOptionSelected: () -> Unit,
     requestDisplayInfo: RequestDisplayInfo,
     providerInfoList: List<ProviderInfo>,
@@ -230,6 +232,7 @@
         openMoreOptionsPage = onMoreOptionSelected,
         sendDataToProvider = onBiometricEntrySelected,
         onCancelFlowAndFinish = onCancelFlowAndFinish,
+        onIllegalStateAndFinish = onIllegalStateAndFinish,
         getRequestDisplayInfo = requestDisplayInfo,
         getProviderInfoList = providerInfoList,
         getProviderDisplayInfo = providerDisplayInfo,
diff --git a/packages/EasterEgg/Android.bp b/packages/EasterEgg/Android.bp
index 6f4f9ca..ec1fe39 100644
--- a/packages/EasterEgg/Android.bp
+++ b/packages/EasterEgg/Android.bp
@@ -83,6 +83,7 @@
 aconfig_declarations {
     name: "easter_egg_flags",
     package: "com.android.egg.flags",
+    container: "system",
     srcs: [
         "easter_egg_flags.aconfig",
     ],
diff --git a/packages/EasterEgg/easter_egg_flags.aconfig b/packages/EasterEgg/easter_egg_flags.aconfig
index 3268a4f..7ddc238 100644
--- a/packages/EasterEgg/easter_egg_flags.aconfig
+++ b/packages/EasterEgg/easter_egg_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.egg.flags"
+container: "system"
 
 flag {
     name: "flag_flag"
diff --git a/packages/PackageInstaller/Android.bp b/packages/PackageInstaller/Android.bp
index 79c810c..bd84b58 100644
--- a/packages/PackageInstaller/Android.bp
+++ b/packages/PackageInstaller/Android.bp
@@ -46,7 +46,6 @@
     sdk_version: "system_current",
     rename_resources_package: false,
     static_libs: [
-        "xz-java",
         "androidx.leanback_leanback",
         "androidx.annotation_annotation",
         "androidx.fragment_fragment",
@@ -79,7 +78,6 @@
     overrides: ["PackageInstaller"],
 
     static_libs: [
-        "xz-java",
         "androidx.leanback_leanback",
         "androidx.fragment_fragment",
         "androidx.lifecycle_lifecycle-livedata",
@@ -112,7 +110,6 @@
     overrides: ["PackageInstaller"],
 
     static_libs: [
-        "xz-java",
         "androidx.leanback_leanback",
         "androidx.annotation_annotation",
         "androidx.fragment_fragment",
diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml
index bf69d3b..05f4d69 100644
--- a/packages/PackageInstaller/AndroidManifest.xml
+++ b/packages/PackageInstaller/AndroidManifest.xml
@@ -146,17 +146,6 @@
                 android:configChanges="mnc|mnc|touchscreen|navigation|screenLayout|screenSize|smallestScreenSize|orientation|locale|keyboard|keyboardHidden|fontScale|uiMode|layoutDirection|density"
                 android:exported="false" />
 
-        <!-- Wearable Components -->
-        <service android:name=".wear.WearPackageInstallerService"
-                 android:permission="com.google.android.permission.INSTALL_WEARABLE_PACKAGES"
-                 android:foregroundServiceType="systemExempted"
-                 android:exported="true"/>
-
-        <provider android:name=".wear.WearPackageIconProvider"
-                  android:authorities="com.google.android.packageinstaller.wear.provider"
-                  android:grantUriPermissions="true"
-                  android:exported="false" />
-
         <receiver android:name="androidx.profileinstaller.ProfileInstallReceiver"
             tools:node="remove" />
 
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/InstallTask.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/InstallTask.java
deleted file mode 100644
index 53a460d..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/InstallTask.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * Copyright (C) 2016 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.packageinstaller.wear;
-
-import android.content.Context;
-import android.content.IntentSender;
-import android.content.pm.PackageInstaller;
-import android.os.Looper;
-import android.os.ParcelFileDescriptor;
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.io.Closeable;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-/**
- * Task that installs an APK. This must not be called on the main thread.
- * This code is based off the Finsky/Wearsky implementation
- */
-public class InstallTask {
-    private static final String TAG = "InstallTask";
-
-    private static final int DEFAULT_BUFFER_SIZE = 8192;
-
-    private final Context mContext;
-    private String mPackageName;
-    private ParcelFileDescriptor mParcelFileDescriptor;
-    private PackageInstallerImpl.InstallListener mCallback;
-    private PackageInstaller.Session mSession;
-    private IntentSender mCommitCallback;
-
-    private Exception mException = null;
-    private int mErrorCode = 0;
-    private String mErrorDesc = null;
-
-    public InstallTask(Context context, String packageName,
-            ParcelFileDescriptor parcelFileDescriptor,
-            PackageInstallerImpl.InstallListener callback, PackageInstaller.Session session,
-            IntentSender commitCallback) {
-        mContext = context;
-        mPackageName = packageName;
-        mParcelFileDescriptor = parcelFileDescriptor;
-        mCallback = callback;
-        mSession = session;
-        mCommitCallback = commitCallback;
-    }
-
-    public boolean isError() {
-        return mErrorCode != InstallerConstants.STATUS_SUCCESS || !TextUtils.isEmpty(mErrorDesc);
-    }
-
-    public void execute() {
-        if (Looper.myLooper() == Looper.getMainLooper()) {
-            throw new IllegalStateException("This method cannot be called from the UI thread.");
-        }
-
-        OutputStream sessionStream = null;
-        try {
-            sessionStream = mSession.openWrite(mPackageName, 0, -1);
-
-            // 2b: Stream the asset to the installer. Note:
-            // Note: writeToOutputStreamFromAsset() always safely closes the input stream
-            writeToOutputStreamFromAsset(sessionStream);
-            mSession.fsync(sessionStream);
-        } catch (Exception e) {
-            mException = e;
-            mErrorCode = InstallerConstants.ERROR_INSTALL_COPY_STREAM;
-            mErrorDesc = "Could not write to stream";
-        } finally {
-            if (sessionStream != null) {
-                // 2c: close output stream
-                try {
-                    sessionStream.close();
-                } catch (Exception e) {
-                    // Ignore otherwise
-                    if (mException == null) {
-                        mException = e;
-                        mErrorCode = InstallerConstants.ERROR_INSTALL_CLOSE_STREAM;
-                        mErrorDesc = "Could not close session stream";
-                    }
-                }
-            }
-        }
-
-        if (mErrorCode != InstallerConstants.STATUS_SUCCESS) {
-            // An error occurred, we're done
-            Log.e(TAG, "Exception while installing " + mPackageName + ": " + mErrorCode + ", "
-                    + mErrorDesc + ", " + mException);
-            mSession.close();
-            mCallback.installFailed(mErrorCode, "[" + mPackageName + "]" + mErrorDesc);
-        } else {
-            // 3. Commit the session (this actually installs it.)  Session map
-            // will be cleaned up in the callback.
-            mCallback.installBeginning();
-            mSession.commit(mCommitCallback);
-            mSession.close();
-        }
-    }
-
-    /**
-     * {@code PackageInstaller} works with streams. Get the {@code FileDescriptor}
-     * corresponding to the {@code Asset} and then write the contents into an
-     * {@code OutputStream} that is passed in.
-     * <br>
-     * The {@code FileDescriptor} is closed but the {@code OutputStream} is not closed.
-     */
-    private boolean writeToOutputStreamFromAsset(OutputStream outputStream) {
-        if (outputStream == null) {
-            mErrorCode = InstallerConstants.ERROR_INSTALL_COPY_STREAM_EXCEPTION;
-            mErrorDesc = "Got a null OutputStream.";
-            return false;
-        }
-
-        if (mParcelFileDescriptor == null || mParcelFileDescriptor.getFileDescriptor() == null)  {
-            mErrorCode = InstallerConstants.ERROR_COULD_NOT_GET_FD;
-            mErrorDesc = "Could not get FD";
-            return false;
-        }
-
-        InputStream inputStream = null;
-        try {
-            byte[] inputBuf = new byte[DEFAULT_BUFFER_SIZE];
-            int bytesRead;
-            inputStream = new ParcelFileDescriptor.AutoCloseInputStream(mParcelFileDescriptor);
-
-            while ((bytesRead = inputStream.read(inputBuf)) > -1) {
-                if (bytesRead > 0) {
-                    outputStream.write(inputBuf, 0, bytesRead);
-                }
-            }
-
-            outputStream.flush();
-        } catch (IOException e) {
-            mErrorCode = InstallerConstants.ERROR_INSTALL_APK_COPY_FAILURE;
-            mErrorDesc = "Reading from Asset FD or writing to temp file failed: " + e;
-            return false;
-        } finally {
-            safeClose(inputStream);
-        }
-
-        return true;
-    }
-
-    /**
-     * Quietly close a closeable resource (e.g. a stream or file). The input may already
-     * be closed and it may even be null.
-     */
-    public static void safeClose(Closeable resource) {
-        if (resource != null) {
-            try {
-                resource.close();
-            } catch (IOException ioe) {
-                // Catch and discard the error
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/InstallerConstants.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/InstallerConstants.java
deleted file mode 100644
index 3daf3d8..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/InstallerConstants.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2016 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.packageinstaller.wear;
-
-/**
- * Constants for Installation / Uninstallation requests.
- * Using the same values as Finsky/Wearsky code for consistency in user analytics of failures
- */
-public class InstallerConstants {
-    /** Request succeeded */
-    public static final int STATUS_SUCCESS = 0;
-
-    /**
-     * The new PackageInstaller also returns a small set of less granular error codes, which
-     * we'll remap to the range -500 and below to keep away from existing installer codes
-     * (which run from -1 to -110).
-     */
-    public final static int ERROR_PACKAGEINSTALLER_BASE = -500;
-
-    public static final int ERROR_COULD_NOT_GET_FD = -603;
-    /** This node is not targeted by this request. */
-
-    /** The install did not complete because could not create PackageInstaller session */
-    public final static int ERROR_INSTALL_CREATE_SESSION = -612;
-    /** The install did not complete because could not open PackageInstaller session  */
-    public final static int ERROR_INSTALL_OPEN_SESSION = -613;
-    /** The install did not complete because could not open PackageInstaller output stream */
-    public final static int ERROR_INSTALL_OPEN_STREAM = -614;
-    /** The install did not complete because of an exception while streaming bytes */
-    public final static int ERROR_INSTALL_COPY_STREAM_EXCEPTION = -615;
-    /** The install did not complete because of an unexpected exception from PackageInstaller */
-    public final static int ERROR_INSTALL_SESSION_EXCEPTION = -616;
-    /** The install did not complete because of an unexpected userActionRequired callback */
-    public final static int ERROR_INSTALL_USER_ACTION_REQUIRED = -617;
-    /** The install did not complete because of an unexpected broadcast (missing fields) */
-    public final static int ERROR_INSTALL_MALFORMED_BROADCAST = -618;
-    /** The install did not complete because of an error while copying from downloaded file */
-    public final static int ERROR_INSTALL_APK_COPY_FAILURE = -619;
-    /** The install did not complete because of an error while copying to the PackageInstaller
-     * output stream */
-    public final static int ERROR_INSTALL_COPY_STREAM = -620;
-    /** The install did not complete because of an error while closing the PackageInstaller
-     * output stream */
-    public final static int ERROR_INSTALL_CLOSE_STREAM = -621;
-}
\ No newline at end of file
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerFactory.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerFactory.java
deleted file mode 100644
index bdc22cf..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerFactory.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2016 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.packageinstaller.wear;
-
-import android.content.Context;
-
-/**
- * Factory that creates a Package Installer.
- */
-public class PackageInstallerFactory {
-    private static PackageInstallerImpl sPackageInstaller;
-
-    /**
-     * Return the PackageInstaller shared object. {@code init} should have already been called.
-     */
-    public synchronized static PackageInstallerImpl getPackageInstaller(Context context) {
-        if (sPackageInstaller == null) {
-            sPackageInstaller = new PackageInstallerImpl(context);
-        }
-        return sPackageInstaller;
-    }
-}
\ No newline at end of file
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerImpl.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerImpl.java
deleted file mode 100644
index 1e37f15..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerImpl.java
+++ /dev/null
@@ -1,327 +0,0 @@
-/*
- * Copyright (C) 2016 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.packageinstaller.wear;
-
-import android.annotation.TargetApi;
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.IntentSender;
-import android.content.pm.PackageInstaller;
-import android.os.Build;
-import android.os.ParcelFileDescriptor;
-import android.util.Log;
-
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Implementation of package manager installation using modern PackageInstaller api.
- *
- * Heavily copied from Wearsky/Finsky implementation
- */
-@TargetApi(Build.VERSION_CODES.LOLLIPOP)
-public class PackageInstallerImpl {
-    private static final String TAG = "PackageInstallerImpl";
-
-    /** Intent actions used for broadcasts from PackageInstaller back to the local receiver */
-    private static final String ACTION_INSTALL_COMMIT =
-            "com.android.vending.INTENT_PACKAGE_INSTALL_COMMIT";
-
-    private final Context mContext;
-    private final PackageInstaller mPackageInstaller;
-    private final Map<String, PackageInstaller.SessionInfo> mSessionInfoMap;
-    private final Map<String, PackageInstaller.Session> mOpenSessionMap;
-
-    public PackageInstallerImpl(Context context) {
-        mContext = context.getApplicationContext();
-        mPackageInstaller = mContext.getPackageManager().getPackageInstaller();
-
-        // Capture a map of known sessions
-        // This list will be pruned a bit later (stale sessions will be canceled)
-        mSessionInfoMap = new HashMap<String, PackageInstaller.SessionInfo>();
-        List<PackageInstaller.SessionInfo> mySessions = mPackageInstaller.getMySessions();
-        for (int i = 0; i < mySessions.size(); i++) {
-            PackageInstaller.SessionInfo sessionInfo = mySessions.get(i);
-            String packageName = sessionInfo.getAppPackageName();
-            PackageInstaller.SessionInfo oldInfo = mSessionInfoMap.put(packageName, sessionInfo);
-
-            // Checking for old info is strictly for logging purposes
-            if (oldInfo != null) {
-                Log.w(TAG, "Multiple sessions for " + packageName + " found. Removing " + oldInfo
-                        .getSessionId() + " & keeping " + mySessions.get(i).getSessionId());
-            }
-        }
-        mOpenSessionMap = new HashMap<String, PackageInstaller.Session>();
-    }
-
-    /**
-     * This callback will be made after an installation attempt succeeds or fails.
-     */
-    public interface InstallListener {
-        /**
-         * This callback signals that preflight checks have succeeded and installation
-         * is beginning.
-         */
-        void installBeginning();
-
-        /**
-         * This callback signals that installation has completed.
-         */
-        void installSucceeded();
-
-        /**
-         * This callback signals that installation has failed.
-         */
-        void installFailed(int errorCode, String errorDesc);
-    }
-
-    /**
-     * This is a placeholder implementation that bundles an entire "session" into a single
-     * call. This will be replaced by more granular versions that allow longer session lifetimes,
-     * download progress tracking, etc.
-     *
-     * This must not be called on main thread.
-     */
-    public void install(final String packageName, ParcelFileDescriptor parcelFileDescriptor,
-            final InstallListener callback) {
-        // 0. Generic try/catch block because I am not really sure what exceptions (other than
-        // IOException) might be thrown by PackageInstaller and I want to handle them
-        // at least slightly gracefully.
-        try {
-            // 1. Create or recover a session, and open it
-            // Try recovery first
-            PackageInstaller.Session session = null;
-            PackageInstaller.SessionInfo sessionInfo = mSessionInfoMap.get(packageName);
-            if (sessionInfo != null) {
-                // See if it's openable, or already held open
-                session = getSession(packageName);
-            }
-            // If open failed, or there was no session, create a new one and open it.
-            // If we cannot create or open here, the failure is terminal.
-            if (session == null) {
-                try {
-                    innerCreateSession(packageName);
-                } catch (IOException ioe) {
-                    Log.e(TAG, "Can't create session for " + packageName + ": " + ioe.getMessage());
-                    callback.installFailed(InstallerConstants.ERROR_INSTALL_CREATE_SESSION,
-                            "Could not create session");
-                    mSessionInfoMap.remove(packageName);
-                    return;
-                }
-                sessionInfo = mSessionInfoMap.get(packageName);
-                try {
-                    session = mPackageInstaller.openSession(sessionInfo.getSessionId());
-                    mOpenSessionMap.put(packageName, session);
-                } catch (SecurityException se) {
-                    Log.e(TAG, "Can't open session for " + packageName + ": " + se.getMessage());
-                    callback.installFailed(InstallerConstants.ERROR_INSTALL_OPEN_SESSION,
-                            "Can't open session");
-                    mSessionInfoMap.remove(packageName);
-                    return;
-                }
-            }
-
-            // 2. Launch task to handle file operations.
-            InstallTask task = new InstallTask( mContext, packageName, parcelFileDescriptor,
-                    callback, session,
-                    getCommitCallback(packageName, sessionInfo.getSessionId(), callback));
-            task.execute();
-            if (task.isError()) {
-                cancelSession(sessionInfo.getSessionId(), packageName);
-            }
-        } catch (Exception e) {
-            Log.e(TAG, "Unexpected exception while installing: " + packageName + ": "
-                    + e.getMessage());
-            callback.installFailed(InstallerConstants.ERROR_INSTALL_SESSION_EXCEPTION,
-                    "Unexpected exception while installing " + packageName);
-        }
-    }
-
-    /**
-     * Retrieve an existing session. Will open if needed, but does not attempt to create.
-     */
-    private PackageInstaller.Session getSession(String packageName) {
-        // Check for already-open session
-        PackageInstaller.Session session = mOpenSessionMap.get(packageName);
-        if (session != null) {
-            try {
-                // Probe the session to ensure that it's still open. This may or may not
-                // throw (if non-open), but it may serve as a canary for stale sessions.
-                session.getNames();
-                return session;
-            } catch (IOException ioe) {
-                Log.e(TAG, "Stale open session for " + packageName + ": " + ioe.getMessage());
-                mOpenSessionMap.remove(packageName);
-            } catch (SecurityException se) {
-                Log.e(TAG, "Stale open session for " + packageName + ": " + se.getMessage());
-                mOpenSessionMap.remove(packageName);
-            }
-        }
-        // Check to see if this is a known session
-        PackageInstaller.SessionInfo sessionInfo = mSessionInfoMap.get(packageName);
-        if (sessionInfo == null) {
-            return null;
-        }
-        // Try to open it. If we fail here, assume that the SessionInfo was stale.
-        try {
-            session = mPackageInstaller.openSession(sessionInfo.getSessionId());
-        } catch (SecurityException se) {
-            Log.w(TAG, "SessionInfo was stale for " + packageName + " - deleting info");
-            mSessionInfoMap.remove(packageName);
-            return null;
-        } catch (IOException ioe) {
-            Log.w(TAG, "IOException opening old session for " + ioe.getMessage()
-                    + " - deleting info");
-            mSessionInfoMap.remove(packageName);
-            return null;
-        }
-        mOpenSessionMap.put(packageName, session);
-        return session;
-    }
-
-    /** This version throws an IOException when the session cannot be created */
-    private void innerCreateSession(String packageName) throws IOException {
-        if (mSessionInfoMap.containsKey(packageName)) {
-            Log.w(TAG, "Creating session for " + packageName + " when one already exists");
-            return;
-        }
-        PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
-                PackageInstaller.SessionParams.MODE_FULL_INSTALL);
-        params.setAppPackageName(packageName);
-
-        // IOException may be thrown at this point
-        int sessionId = mPackageInstaller.createSession(params);
-        PackageInstaller.SessionInfo sessionInfo = mPackageInstaller.getSessionInfo(sessionId);
-        mSessionInfoMap.put(packageName, sessionInfo);
-    }
-
-    /**
-     * Cancel a session based on its sessionId. Package name is for logging only.
-     */
-    private void cancelSession(int sessionId, String packageName) {
-        // Close if currently held open
-        closeSession(packageName);
-        // Remove local record
-        mSessionInfoMap.remove(packageName);
-        try {
-            mPackageInstaller.abandonSession(sessionId);
-        } catch (SecurityException se) {
-            // The session no longer exists, so we can exit quietly.
-            return;
-        }
-    }
-
-    /**
-     * Close a session if it happens to be held open.
-     */
-    private void closeSession(String packageName) {
-        PackageInstaller.Session session = mOpenSessionMap.remove(packageName);
-        if (session != null) {
-            // Unfortunately close() is not idempotent. Try our best to make this safe.
-            try {
-                session.close();
-            } catch (Exception e) {
-                Log.w(TAG, "Unexpected error closing session for " + packageName + ": "
-                        + e.getMessage());
-            }
-        }
-    }
-
-    /**
-     * Creates a commit callback for the package install that's underway. This will be called
-     * some time after calling session.commit() (above).
-     */
-    private IntentSender getCommitCallback(final String packageName, final int sessionId,
-            final InstallListener callback) {
-        // Create a single-use broadcast receiver
-        BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                mContext.unregisterReceiver(this);
-                handleCommitCallback(intent, packageName, sessionId, callback);
-            }
-        };
-        // Create a matching intent-filter and register the receiver
-        String action = ACTION_INSTALL_COMMIT + "." + packageName;
-        IntentFilter intentFilter = new IntentFilter();
-        intentFilter.addAction(action);
-        mContext.registerReceiver(broadcastReceiver, intentFilter,
-                Context.RECEIVER_EXPORTED);
-
-        // Create a matching PendingIntent and use it to generate the IntentSender
-        Intent broadcastIntent = new Intent(action).setPackage(mContext.getPackageName());
-        PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, packageName.hashCode(),
-                broadcastIntent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT
-                        | PendingIntent.FLAG_MUTABLE);
-        return pendingIntent.getIntentSender();
-    }
-
-    /**
-     * Examine the extras to determine information about the package update/install, decode
-     * the result, and call the appropriate callback.
-     *
-     * @param intent The intent, which the PackageInstaller will have added Extras to
-     * @param packageName The package name we created the receiver for
-     * @param sessionId The session Id we created the receiver for
-     * @param callback The callback to report success/failure to
-     */
-    private void handleCommitCallback(Intent intent, String packageName, int sessionId,
-            InstallListener callback) {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "Installation of " + packageName + " finished with extras "
-                    + intent.getExtras());
-        }
-        String statusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
-        int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, Integer.MIN_VALUE);
-        if (status == PackageInstaller.STATUS_SUCCESS) {
-            cancelSession(sessionId, packageName);
-            callback.installSucceeded();
-        } else if (status == -1 /*PackageInstaller.STATUS_USER_ACTION_REQUIRED*/) {
-            // TODO - use the constant when the correct/final name is in the SDK
-            // TODO This is unexpected, so we are treating as failure for now
-            cancelSession(sessionId, packageName);
-            callback.installFailed(InstallerConstants.ERROR_INSTALL_USER_ACTION_REQUIRED,
-                    "Unexpected: user action required");
-        } else {
-            cancelSession(sessionId, packageName);
-            int errorCode = getPackageManagerErrorCode(status);
-            Log.e(TAG, "Error " + errorCode + " while installing " + packageName + ": "
-                    + statusMessage);
-            callback.installFailed(errorCode, null);
-        }
-    }
-
-    private int getPackageManagerErrorCode(int status) {
-        // This is a hack: because PackageInstaller now reports error codes
-        // with small positive values, we need to remap them into a space
-        // that is more compatible with the existing package manager error codes.
-        // See https://sites.google.com/a/google.com/universal-store/documentation
-        //       /android-client/download-error-codes
-        int errorCode;
-        if (status == Integer.MIN_VALUE) {
-            errorCode = InstallerConstants.ERROR_INSTALL_MALFORMED_BROADCAST;
-        } else {
-            errorCode = InstallerConstants.ERROR_PACKAGEINSTALLER_BASE - status;
-        }
-        return errorCode;
-    }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageArgs.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageArgs.java
deleted file mode 100644
index 2c289b2..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageArgs.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright (C) 2015 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.packageinstaller.wear;
-
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Bundle;
-
-/**
- * Installation Util that contains a list of parameters that are needed for
- * installing/uninstalling.
- */
-public class WearPackageArgs {
-    private static final String KEY_PACKAGE_NAME =
-            "com.google.android.clockwork.EXTRA_PACKAGE_NAME";
-    private static final String KEY_ASSET_URI =
-            "com.google.android.clockwork.EXTRA_ASSET_URI";
-    private static final String KEY_START_ID =
-            "com.google.android.clockwork.EXTRA_START_ID";
-    private static final String KEY_PERM_URI =
-            "com.google.android.clockwork.EXTRA_PERM_URI";
-    private static final String KEY_CHECK_PERMS =
-            "com.google.android.clockwork.EXTRA_CHECK_PERMS";
-    private static final String KEY_SKIP_IF_SAME_VERSION =
-            "com.google.android.clockwork.EXTRA_SKIP_IF_SAME_VERSION";
-    private static final String KEY_COMPRESSION_ALG =
-            "com.google.android.clockwork.EXTRA_KEY_COMPRESSION_ALG";
-    private static final String KEY_COMPANION_SDK_VERSION =
-            "com.google.android.clockwork.EXTRA_KEY_COMPANION_SDK_VERSION";
-    private static final String KEY_COMPANION_DEVICE_VERSION =
-            "com.google.android.clockwork.EXTRA_KEY_COMPANION_DEVICE_VERSION";
-    private static final String KEY_SHOULD_CHECK_GMS_DEPENDENCY =
-            "com.google.android.clockwork.EXTRA_KEY_SHOULD_CHECK_GMS_DEPENDENCY";
-    private static final String KEY_SKIP_IF_LOWER_VERSION =
-            "com.google.android.clockwork.EXTRA_SKIP_IF_LOWER_VERSION";
-
-    public static String getPackageName(Bundle b) {
-        return b.getString(KEY_PACKAGE_NAME);
-    }
-
-    public static Bundle setPackageName(Bundle b, String packageName) {
-        b.putString(KEY_PACKAGE_NAME, packageName);
-        return b;
-    }
-
-    public static Uri getAssetUri(Bundle b) {
-        return b.getParcelable(KEY_ASSET_URI);
-    }
-
-    public static Uri getPermUri(Bundle b) {
-        return b.getParcelable(KEY_PERM_URI);
-    }
-
-    public static boolean checkPerms(Bundle b) {
-        return b.getBoolean(KEY_CHECK_PERMS);
-    }
-
-    public static boolean skipIfSameVersion(Bundle b) {
-        return b.getBoolean(KEY_SKIP_IF_SAME_VERSION);
-    }
-
-    public static int getCompanionSdkVersion(Bundle b) {
-        return b.getInt(KEY_COMPANION_SDK_VERSION);
-    }
-
-    public static int getCompanionDeviceVersion(Bundle b) {
-        return b.getInt(KEY_COMPANION_DEVICE_VERSION);
-    }
-
-    public static String getCompressionAlg(Bundle b) {
-        return b.getString(KEY_COMPRESSION_ALG);
-    }
-
-    public static int getStartId(Bundle b) {
-        return b.getInt(KEY_START_ID);
-    }
-
-    public static boolean skipIfLowerVersion(Bundle b) {
-        return b.getBoolean(KEY_SKIP_IF_LOWER_VERSION, false);
-    }
-
-    public static Bundle setStartId(Bundle b, int startId) {
-        b.putInt(KEY_START_ID, startId);
-        return b;
-    }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageIconProvider.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageIconProvider.java
deleted file mode 100644
index 02b9d29..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageIconProvider.java
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- * Copyright (C) 2015 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.packageinstaller.wear;
-
-import android.annotation.TargetApi;
-import android.app.ActivityManager;
-import android.content.ContentProvider;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Binder;
-import android.os.Build;
-import android.os.ParcelFileDescriptor;
-import android.util.Log;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.util.List;
-
-import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-
-public class WearPackageIconProvider extends ContentProvider {
-    private static final String TAG = "WearPackageIconProvider";
-    public static final String AUTHORITY = "com.google.android.packageinstaller.wear.provider";
-
-    private static final String REQUIRED_PERMISSION =
-            "com.google.android.permission.INSTALL_WEARABLE_PACKAGES";
-
-    /** MIME types. */
-    public static final String ICON_TYPE = "vnd.android.cursor.item/cw_package_icon";
-
-    @Override
-    public boolean onCreate() {
-        return true;
-    }
-
-    @Override
-    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
-            String sortOrder) {
-        throw new UnsupportedOperationException("Query is not supported.");
-    }
-
-    @Override
-    public String getType(Uri uri) {
-        if (uri == null) {
-            throw new IllegalArgumentException("URI passed in is null.");
-        }
-
-        if (AUTHORITY.equals(uri.getEncodedAuthority())) {
-            return ICON_TYPE;
-        }
-        return null;
-    }
-
-    @Override
-    public Uri insert(Uri uri, ContentValues values) {
-        throw new UnsupportedOperationException("Insert is not supported.");
-    }
-
-    @Override
-    public int delete(Uri uri, String selection, String[] selectionArgs) {
-        if (uri == null) {
-            throw new IllegalArgumentException("URI passed in is null.");
-        }
-
-        enforcePermissions(uri);
-
-        if (ICON_TYPE.equals(getType(uri))) {
-            final File file = WearPackageUtil.getIconFile(
-                    this.getContext().getApplicationContext(), getPackageNameFromUri(uri));
-            if (file != null) {
-                file.delete();
-            }
-        }
-
-        return 0;
-    }
-
-    @Override
-    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
-        throw new UnsupportedOperationException("Update is not supported.");
-    }
-
-    @Override
-    public ParcelFileDescriptor openFile(
-            Uri uri, @SuppressWarnings("unused") String mode) throws FileNotFoundException {
-        if (uri == null) {
-            throw new IllegalArgumentException("URI passed in is null.");
-        }
-
-        enforcePermissions(uri);
-
-        if (ICON_TYPE.equals(getType(uri))) {
-            final File file = WearPackageUtil.getIconFile(
-                    this.getContext().getApplicationContext(), getPackageNameFromUri(uri));
-            if (file != null) {
-                return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
-            }
-        }
-        return null;
-    }
-
-    public static Uri getUriForPackage(final String packageName) {
-        return Uri.parse("content://" + AUTHORITY + "/icons/" + packageName + ".icon");
-    }
-
-    private String getPackageNameFromUri(Uri uri) {
-        if (uri == null) {
-            return null;
-        }
-        List<String> pathSegments = uri.getPathSegments();
-        String packageName = pathSegments.get(pathSegments.size() - 1);
-
-        if (packageName.endsWith(".icon")) {
-            packageName = packageName.substring(0, packageName.lastIndexOf("."));
-        }
-        return packageName;
-    }
-
-    /**
-     * Make sure the calling app is either a system app or the same app or has the right permission.
-     * @throws SecurityException if the caller has insufficient permissions.
-     */
-    @TargetApi(Build.VERSION_CODES.BASE_1_1)
-    private void enforcePermissions(Uri uri) {
-        // Redo some of the permission check in {@link ContentProvider}. Just add an extra check to
-        // allow System process to access this provider.
-        Context context = getContext();
-        final int pid = Binder.getCallingPid();
-        final int uid = Binder.getCallingUid();
-        final int myUid = android.os.Process.myUid();
-
-        if (uid == myUid || isSystemApp(context, pid)) {
-            return;
-        }
-
-        if (context.checkPermission(REQUIRED_PERMISSION, pid, uid) == PERMISSION_GRANTED) {
-            return;
-        }
-
-        // last chance, check against any uri grants
-        if (context.checkUriPermission(uri, pid, uid, Intent.FLAG_GRANT_READ_URI_PERMISSION)
-                == PERMISSION_GRANTED) {
-            return;
-        }
-
-        throw new SecurityException("Permission Denial: reading "
-                + getClass().getName() + " uri " + uri + " from pid=" + pid
-                + ", uid=" + uid);
-    }
-
-    /**
-     * From the pid of the calling process, figure out whether this is a system app or not. We do
-     * this by checking the application information corresponding to the pid and then checking if
-     * FLAG_SYSTEM is set.
-     */
-    @TargetApi(Build.VERSION_CODES.CUPCAKE)
-    private boolean isSystemApp(Context context, int pid) {
-        // Get the Activity Manager Object
-        ActivityManager aManager =
-                (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
-        // Get the list of running Applications
-        List<ActivityManager.RunningAppProcessInfo> rapInfoList =
-                aManager.getRunningAppProcesses();
-        for (ActivityManager.RunningAppProcessInfo rapInfo : rapInfoList) {
-            if (rapInfo.pid == pid) {
-                try {
-                    PackageInfo pkgInfo = context.getPackageManager().getPackageInfo(
-                            rapInfo.pkgList[0], 0);
-                    if (pkgInfo != null && pkgInfo.applicationInfo != null &&
-                            (pkgInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
-                        Log.d(TAG, pid + " is a system app.");
-                        return true;
-                    }
-                } catch (PackageManager.NameNotFoundException e) {
-                    Log.e(TAG, "Could not find package information.", e);
-                    return false;
-                }
-            }
-        }
-        return false;
-    }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageInstallerService.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageInstallerService.java
deleted file mode 100644
index ae0f4ec..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageInstallerService.java
+++ /dev/null
@@ -1,621 +0,0 @@
-/*
- * Copyright (C) 2015 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.packageinstaller.wear;
-
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.FeatureInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageInstaller;
-import android.content.pm.PackageManager;
-import android.content.pm.VersionedPackage;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.IBinder;
-import android.os.Looper;
-import android.os.Message;
-import android.os.ParcelFileDescriptor;
-import android.os.PowerManager;
-import android.os.Process;
-import android.util.ArrayMap;
-import android.util.Log;
-import android.util.Pair;
-import androidx.annotation.Nullable;
-import com.android.packageinstaller.DeviceUtils;
-import com.android.packageinstaller.PackageUtil;
-import com.android.packageinstaller.R;
-import com.android.packageinstaller.common.EventResultPersister;
-import com.android.packageinstaller.common.UninstallEventReceiver;
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * Service that will install/uninstall packages. It will check for permissions and features as well.
- *
- * -----------
- *
- * Debugging information:
- *
- *  Install Action example:
- *  adb shell am startservice -a com.android.packageinstaller.wear.INSTALL_PACKAGE \
- *     -d package://com.google.android.gms \
- *     --eu com.google.android.clockwork.EXTRA_ASSET_URI content://com.google.android.clockwork.home.provider/host/com.google.android.wearable.app/wearable/com.google.android.gms/apk \
- *     --es android.intent.extra.INSTALLER_PACKAGE_NAME com.google.android.gms \
- *     --ez com.google.android.clockwork.EXTRA_CHECK_PERMS false \
- *     --eu com.google.android.clockwork.EXTRA_PERM_URI content://com.google.android.clockwork.home.provider/host/com.google.android.wearable.app/permissions \
- *     com.android.packageinstaller/com.android.packageinstaller.wear.WearPackageInstallerService
- *
- *  Uninstall Action example:
- *  adb shell am startservice -a com.android.packageinstaller.wear.UNINSTALL_PACKAGE \
- *     -d package://com.google.android.gms \
- *     com.android.packageinstaller/com.android.packageinstaller.wear.WearPackageInstallerService
- *
- *  Retry GMS:
- *  adb shell am startservice -a com.android.packageinstaller.wear.RETRY_GMS \
- *     com.android.packageinstaller/com.android.packageinstaller.wear.WearPackageInstallerService
- */
-public class WearPackageInstallerService extends Service
-        implements EventResultPersister.EventResultObserver {
-    private static final String TAG = "WearPkgInstallerService";
-
-    private static final String WEAR_APPS_CHANNEL = "wear_app_install_uninstall";
-    private static final String BROADCAST_ACTION =
-            "com.android.packageinstaller.ACTION_UNINSTALL_COMMIT";
-
-    private final int START_INSTALL = 1;
-    private final int START_UNINSTALL = 2;
-
-    private int mInstallNotificationId = 1;
-    private final Map<String, Integer> mNotifIdMap = new ArrayMap<>();
-    private final Map<Integer, UninstallParams> mServiceIdToParams = new HashMap<>();
-
-    private class UninstallParams {
-        public String mPackageName;
-        public PowerManager.WakeLock mLock;
-
-        UninstallParams(String packageName, PowerManager.WakeLock lock) {
-            mPackageName = packageName;
-            mLock = lock;
-        }
-    }
-
-    private final class ServiceHandler extends Handler {
-        public ServiceHandler(Looper looper) {
-            super(looper);
-        }
-
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case START_INSTALL:
-                    installPackage(msg.getData());
-                    break;
-                case START_UNINSTALL:
-                    uninstallPackage(msg.getData());
-                    break;
-            }
-        }
-    }
-    private ServiceHandler mServiceHandler;
-    private NotificationChannel mNotificationChannel;
-    private static volatile PowerManager.WakeLock lockStatic = null;
-
-    @Override
-    public IBinder onBind(Intent intent) {
-        return null;
-    }
-
-    @Override
-    public void onCreate() {
-        super.onCreate();
-        HandlerThread thread = new HandlerThread("PackageInstallerThread",
-                Process.THREAD_PRIORITY_BACKGROUND);
-        thread.start();
-
-        mServiceHandler = new ServiceHandler(thread.getLooper());
-    }
-
-    @Override
-    public int onStartCommand(Intent intent, int flags, int startId) {
-        if (!DeviceUtils.isWear(this)) {
-            Log.w(TAG, "Not running on wearable.");
-            finishServiceEarly(startId);
-            return START_NOT_STICKY;
-        }
-
-        if (intent == null) {
-            Log.w(TAG, "Got null intent.");
-            finishServiceEarly(startId);
-            return START_NOT_STICKY;
-        }
-
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "Got install/uninstall request " + intent);
-        }
-
-        Uri packageUri = intent.getData();
-        if (packageUri == null) {
-            Log.e(TAG, "No package URI in intent");
-            finishServiceEarly(startId);
-            return START_NOT_STICKY;
-        }
-
-        final String packageName = WearPackageUtil.getSanitizedPackageName(packageUri);
-        if (packageName == null) {
-            Log.e(TAG, "Invalid package name in URI (expected package:<pkgName>): " + packageUri);
-            finishServiceEarly(startId);
-            return START_NOT_STICKY;
-        }
-
-        PowerManager.WakeLock lock = getLock(this.getApplicationContext());
-        if (!lock.isHeld()) {
-            lock.acquire();
-        }
-
-        Bundle intentBundle = intent.getExtras();
-        if (intentBundle == null) {
-            intentBundle = new Bundle();
-        }
-        WearPackageArgs.setStartId(intentBundle, startId);
-        WearPackageArgs.setPackageName(intentBundle, packageName);
-        Message msg;
-        String notifTitle;
-        if (Intent.ACTION_INSTALL_PACKAGE.equals(intent.getAction())) {
-            msg = mServiceHandler.obtainMessage(START_INSTALL);
-            notifTitle = getString(R.string.installing);
-        } else if (Intent.ACTION_UNINSTALL_PACKAGE.equals(intent.getAction())) {
-            msg = mServiceHandler.obtainMessage(START_UNINSTALL);
-            notifTitle = getString(R.string.uninstalling);
-        } else {
-            Log.e(TAG, "Unknown action : " + intent.getAction());
-            finishServiceEarly(startId);
-            return START_NOT_STICKY;
-        }
-        Pair<Integer, Notification> notifPair = buildNotification(packageName, notifTitle);
-        startForeground(notifPair.first, notifPair.second);
-        msg.setData(intentBundle);
-        mServiceHandler.sendMessage(msg);
-        return START_NOT_STICKY;
-    }
-
-    private void installPackage(Bundle argsBundle) {
-        int startId = WearPackageArgs.getStartId(argsBundle);
-        final String packageName = WearPackageArgs.getPackageName(argsBundle);
-        final Uri assetUri = WearPackageArgs.getAssetUri(argsBundle);
-        final Uri permUri = WearPackageArgs.getPermUri(argsBundle);
-        boolean checkPerms = WearPackageArgs.checkPerms(argsBundle);
-        boolean skipIfSameVersion = WearPackageArgs.skipIfSameVersion(argsBundle);
-        int companionSdkVersion = WearPackageArgs.getCompanionSdkVersion(argsBundle);
-        int companionDeviceVersion = WearPackageArgs.getCompanionDeviceVersion(argsBundle);
-        String compressionAlg = WearPackageArgs.getCompressionAlg(argsBundle);
-        boolean skipIfLowerVersion = WearPackageArgs.skipIfLowerVersion(argsBundle);
-
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "Installing package: " + packageName + ", assetUri: " + assetUri +
-                    ",permUri: " + permUri + ", startId: " + startId + ", checkPerms: " +
-                    checkPerms + ", skipIfSameVersion: " + skipIfSameVersion +
-                    ", compressionAlg: " + compressionAlg + ", companionSdkVersion: " +
-                    companionSdkVersion + ", companionDeviceVersion: " + companionDeviceVersion +
-                    ", skipIfLowerVersion: " + skipIfLowerVersion);
-        }
-        final PackageManager pm = getPackageManager();
-        File tempFile = null;
-        PowerManager.WakeLock lock = getLock(this.getApplicationContext());
-        boolean messageSent = false;
-        try {
-            PackageInfo existingPkgInfo = null;
-            try {
-                existingPkgInfo = pm.getPackageInfo(packageName,
-                        PackageManager.MATCH_ANY_USER | PackageManager.GET_PERMISSIONS);
-                if (existingPkgInfo != null) {
-                    if (Log.isLoggable(TAG, Log.DEBUG)) {
-                        Log.d(TAG, "Replacing package:" + packageName);
-                    }
-                }
-            } catch (PackageManager.NameNotFoundException e) {
-                // Ignore this exception. We could not find the package, will treat as a new
-                // installation.
-            }
-            // TODO(28021618): This was left as a temp file due to the fact that this code is being
-            //       deprecated and that we need the bare minimum to continue working moving forward
-            //       If this code is used as reference, this permission logic might want to be
-            //       reworked to use a stream instead of a file so that we don't need to write a
-            //       file at all.  Note that there might be some trickiness with opening a stream
-            //       for multiple users.
-            ParcelFileDescriptor parcelFd = getContentResolver()
-                    .openFileDescriptor(assetUri, "r");
-            tempFile = WearPackageUtil.getFileFromFd(WearPackageInstallerService.this,
-                    parcelFd, packageName, compressionAlg);
-            if (tempFile == null) {
-                Log.e(TAG, "Could not create a temp file from FD for " + packageName);
-                return;
-            }
-            PackageInfo pkgInfo = PackageUtil.getPackageInfo(this, tempFile,
-                    PackageManager.GET_PERMISSIONS | PackageManager.GET_CONFIGURATIONS);
-            if (pkgInfo == null) {
-                Log.e(TAG, "Could not parse apk information for " + packageName);
-                return;
-            }
-
-            if (!pkgInfo.packageName.equals(packageName)) {
-                Log.e(TAG, "Wearable Package Name has to match what is provided for " +
-                        packageName);
-                return;
-            }
-
-            ApplicationInfo appInfo = pkgInfo.applicationInfo;
-            appInfo.sourceDir = tempFile.getPath();
-            appInfo.publicSourceDir = tempFile.getPath();
-            getLabelAndUpdateNotification(packageName,
-                    getString(R.string.installing_app, appInfo.loadLabel(pm)));
-
-            List<String> wearablePerms = Arrays.asList(pkgInfo.requestedPermissions);
-
-            // Log if the installed pkg has a higher version number.
-            if (existingPkgInfo != null) {
-                long longVersionCode = pkgInfo.getLongVersionCode();
-                if (existingPkgInfo.getLongVersionCode() == longVersionCode) {
-                    if (skipIfSameVersion) {
-                        Log.w(TAG, "Version number (" + longVersionCode +
-                                ") of new app is equal to existing app for " + packageName +
-                                "; not installing due to versionCheck");
-                        return;
-                    } else {
-                        Log.w(TAG, "Version number of new app (" + longVersionCode +
-                                ") is equal to existing app for " + packageName);
-                    }
-                } else if (existingPkgInfo.getLongVersionCode() > longVersionCode) {
-                    if (skipIfLowerVersion) {
-                        // Starting in Feldspar, we are not going to allow downgrades of any app.
-                        Log.w(TAG, "Version number of new app (" + longVersionCode +
-                                ") is lower than existing app ( "
-                                + existingPkgInfo.getLongVersionCode() +
-                                ") for " + packageName + "; not installing due to versionCheck");
-                        return;
-                    } else {
-                        Log.w(TAG, "Version number of new app (" + longVersionCode +
-                                ") is lower than existing app ( "
-                                + existingPkgInfo.getLongVersionCode() + ") for " + packageName);
-                    }
-                }
-
-                // Following the Android Phone model, we should only check for permissions for any
-                // newly defined perms.
-                if (existingPkgInfo.requestedPermissions != null) {
-                    for (int i = 0; i < existingPkgInfo.requestedPermissions.length; ++i) {
-                        // If the permission is granted, then we will not ask to request it again.
-                        if ((existingPkgInfo.requestedPermissionsFlags[i] &
-                                PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0) {
-                            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                                Log.d(TAG, existingPkgInfo.requestedPermissions[i] +
-                                        " is already granted for " + packageName);
-                            }
-                            wearablePerms.remove(existingPkgInfo.requestedPermissions[i]);
-                        }
-                    }
-                }
-            }
-
-            // Check that the wearable has all the features.
-            boolean hasAllFeatures = true;
-            for (FeatureInfo feature : pkgInfo.reqFeatures) {
-                if (feature.name != null && !pm.hasSystemFeature(feature.name) &&
-                        (feature.flags & FeatureInfo.FLAG_REQUIRED) != 0) {
-                    Log.e(TAG, "Wearable does not have required feature: " + feature +
-                            " for " + packageName);
-                    hasAllFeatures = false;
-                }
-            }
-
-            if (!hasAllFeatures) {
-                return;
-            }
-
-            // Check permissions on both the new wearable package and also on the already installed
-            // wearable package.
-            // If the app is targeting API level 23, we will also start a service in ClockworkHome
-            // which will ultimately prompt the user to accept/reject permissions.
-            if (checkPerms && !checkPermissions(pkgInfo, companionSdkVersion,
-                    companionDeviceVersion, permUri, wearablePerms, tempFile)) {
-                Log.w(TAG, "Wearable does not have enough permissions.");
-                return;
-            }
-
-            // Finally install the package.
-            ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(assetUri, "r");
-            PackageInstallerFactory.getPackageInstaller(this).install(packageName, fd,
-                    new PackageInstallListener(this, lock, startId, packageName));
-
-            messageSent = true;
-            Log.i(TAG, "Sent installation request for " + packageName);
-        } catch (FileNotFoundException e) {
-            Log.e(TAG, "Could not find the file with URI " + assetUri, e);
-        } finally {
-            if (!messageSent) {
-                // Some error happened. If the message has been sent, we can wait for the observer
-                // which will finish the service.
-                if (tempFile != null) {
-                    tempFile.delete();
-                }
-                finishService(lock, startId);
-            }
-        }
-    }
-
-    // TODO: This was left using the old PackageManager API due to the fact that this code is being
-    //       deprecated and that we need the bare minimum to continue working moving forward
-    //       If this code is used as reference, this logic should be reworked to use the new
-    //       PackageInstaller APIs similar to how installPackage was reworked
-    private void uninstallPackage(Bundle argsBundle) {
-        int startId = WearPackageArgs.getStartId(argsBundle);
-        final String packageName = WearPackageArgs.getPackageName(argsBundle);
-
-        PowerManager.WakeLock lock = getLock(this.getApplicationContext());
-
-        UninstallParams params = new UninstallParams(packageName, lock);
-        mServiceIdToParams.put(startId, params);
-
-        final PackageManager pm = getPackageManager();
-        try {
-            PackageInfo pkgInfo = pm.getPackageInfo(packageName, 0);
-            getLabelAndUpdateNotification(packageName,
-                    getString(R.string.uninstalling_app, pkgInfo.applicationInfo.loadLabel(pm)));
-
-            int uninstallId = UninstallEventReceiver.addObserver(this,
-                    EventResultPersister.GENERATE_NEW_ID, this);
-
-            Intent broadcastIntent = new Intent(BROADCAST_ACTION);
-            broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
-            broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, uninstallId);
-            broadcastIntent.putExtra(EventResultPersister.EXTRA_SERVICE_ID, startId);
-            broadcastIntent.setPackage(getPackageName());
-
-            PendingIntent pendingIntent = PendingIntent.getBroadcast(this, uninstallId,
-                    broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT
-                            | PendingIntent.FLAG_MUTABLE);
-
-            // Found package, send uninstall request.
-            pm.getPackageInstaller().uninstall(
-                    new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST),
-                    PackageManager.DELETE_ALL_USERS,
-                    pendingIntent.getIntentSender());
-
-            Log.i(TAG, "Sent delete request for " + packageName);
-        } catch (IllegalArgumentException | PackageManager.NameNotFoundException e) {
-            // Couldn't find the package, no need to call uninstall.
-            Log.w(TAG, "Could not find package, not deleting " + packageName, e);
-            finishService(lock, startId);
-        } catch (EventResultPersister.OutOfIdsException e) {
-            Log.e(TAG, "Fails to start uninstall", e);
-            finishService(lock, startId);
-        }
-    }
-
-    @Override
-    public void onResult(int status, int legacyStatus, @Nullable String message, int serviceId) {
-        if (mServiceIdToParams.containsKey(serviceId)) {
-            UninstallParams params = mServiceIdToParams.get(serviceId);
-            try {
-                if (status == PackageInstaller.STATUS_SUCCESS) {
-                    Log.i(TAG, "Package " + params.mPackageName + " was uninstalled.");
-                } else {
-                    Log.e(TAG, "Package uninstall failed " + params.mPackageName
-                            + ", returnCode " + legacyStatus);
-                }
-            } finally {
-                finishService(params.mLock, serviceId);
-            }
-        }
-    }
-
-    private boolean checkPermissions(PackageInfo pkgInfo, int companionSdkVersion,
-            int companionDeviceVersion, Uri permUri, List<String> wearablePermissions,
-            File apkFile) {
-        // Assumption: We are running on Android O.
-        // If the Phone App is targeting M, all permissions may not have been granted to the phone
-        // app. If the Wear App is then not targeting M, there may be permissions that are not
-        // granted on the Phone app (by the user) right now and we cannot just grant it for the Wear
-        // app.
-        if (pkgInfo.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.M) {
-            // Install the app if Wear App is ready for the new perms model.
-            return true;
-        }
-
-        if (!doesWearHaveUngrantedPerms(pkgInfo.packageName, permUri, wearablePermissions)) {
-            // All permissions requested by the watch are already granted on the phone, no need
-            // to do anything.
-            return true;
-        }
-
-        // Log an error if Wear is targeting < 23 and phone is targeting >= 23.
-        if (companionSdkVersion == 0 || companionSdkVersion >= Build.VERSION_CODES.M) {
-            Log.e(TAG, "MNC: Wear app's targetSdkVersion should be at least 23, if "
-                    + "phone app is targeting at least 23, will continue.");
-        }
-
-        return false;
-    }
-
-    /**
-     * Given a {@string packageName} corresponding to a phone app, query the provider for all the
-     * perms that are granted.
-     *
-     * @return true if the Wear App has any perms that have not been granted yet on the phone side.
-     * @return true if there is any error cases.
-     */
-    private boolean doesWearHaveUngrantedPerms(String packageName, Uri permUri,
-            List<String> wearablePermissions) {
-        if (permUri == null) {
-            Log.e(TAG, "Permission URI is null");
-            // Pretend there is an ungranted permission to avoid installing for error cases.
-            return true;
-        }
-        Cursor permCursor = getContentResolver().query(permUri, null, null, null, null);
-        if (permCursor == null) {
-            Log.e(TAG, "Could not get the cursor for the permissions");
-            // Pretend there is an ungranted permission to avoid installing for error cases.
-            return true;
-        }
-
-        Set<String> grantedPerms = new HashSet<>();
-        Set<String> ungrantedPerms = new HashSet<>();
-        while(permCursor.moveToNext()) {
-            // Make sure that the MatrixCursor returned by the ContentProvider has 2 columns and
-            // verify their types.
-            if (permCursor.getColumnCount() == 2
-                    && Cursor.FIELD_TYPE_STRING == permCursor.getType(0)
-                    && Cursor.FIELD_TYPE_INTEGER == permCursor.getType(1)) {
-                String perm = permCursor.getString(0);
-                Integer granted = permCursor.getInt(1);
-                if (granted == 1) {
-                    grantedPerms.add(perm);
-                } else {
-                    ungrantedPerms.add(perm);
-                }
-            }
-        }
-        permCursor.close();
-
-        boolean hasUngrantedPerm = false;
-        for (String wearablePerm : wearablePermissions) {
-            if (!grantedPerms.contains(wearablePerm)) {
-                hasUngrantedPerm = true;
-                if (!ungrantedPerms.contains(wearablePerm)) {
-                    // This is an error condition. This means that the wearable has permissions that
-                    // are not even declared in its host app. This is a developer error.
-                    Log.e(TAG, "Wearable " + packageName + " has a permission \"" + wearablePerm
-                            + "\" that is not defined in the host application's manifest.");
-                } else {
-                    Log.w(TAG, "Wearable " + packageName + " has a permission \"" + wearablePerm +
-                            "\" that is not granted in the host application.");
-                }
-            }
-        }
-        return hasUngrantedPerm;
-    }
-
-    /** Finishes the service after fulfilling obligation to call startForeground. */
-    private void finishServiceEarly(int startId) {
-        Pair<Integer, Notification> notifPair = buildNotification(
-                getApplicationContext().getPackageName(), "");
-        startForeground(notifPair.first, notifPair.second);
-        finishService(null, startId);
-    }
-
-    private void finishService(PowerManager.WakeLock lock, int startId) {
-        if (lock != null && lock.isHeld()) {
-            lock.release();
-        }
-        stopSelf(startId);
-    }
-
-    private synchronized PowerManager.WakeLock getLock(Context context) {
-        if (lockStatic == null) {
-            PowerManager mgr =
-                    (PowerManager) context.getSystemService(Context.POWER_SERVICE);
-            lockStatic = mgr.newWakeLock(
-                    PowerManager.PARTIAL_WAKE_LOCK, context.getClass().getSimpleName());
-            lockStatic.setReferenceCounted(true);
-        }
-        return lockStatic;
-    }
-
-    private class PackageInstallListener implements PackageInstallerImpl.InstallListener {
-        private Context mContext;
-        private PowerManager.WakeLock mWakeLock;
-        private int mStartId;
-        private String mApplicationPackageName;
-        private PackageInstallListener(Context context, PowerManager.WakeLock wakeLock,
-                int startId, String applicationPackageName) {
-            mContext = context;
-            mWakeLock = wakeLock;
-            mStartId = startId;
-            mApplicationPackageName = applicationPackageName;
-        }
-
-        @Override
-        public void installBeginning() {
-            Log.i(TAG, "Package " + mApplicationPackageName + " is being installed.");
-        }
-
-        @Override
-        public void installSucceeded() {
-            try {
-                Log.i(TAG, "Package " + mApplicationPackageName + " was installed.");
-
-                // Delete tempFile from the file system.
-                File tempFile = WearPackageUtil.getTemporaryFile(mContext, mApplicationPackageName);
-                if (tempFile != null) {
-                    tempFile.delete();
-                }
-            } finally {
-                finishService(mWakeLock, mStartId);
-            }
-        }
-
-        @Override
-        public void installFailed(int errorCode, String errorDesc) {
-            Log.e(TAG, "Package install failed " + mApplicationPackageName
-                    + ", errorCode " + errorCode);
-            finishService(mWakeLock, mStartId);
-        }
-    }
-
-    private synchronized Pair<Integer, Notification> buildNotification(final String packageName,
-            final String title) {
-        int notifId;
-        if (mNotifIdMap.containsKey(packageName)) {
-            notifId = mNotifIdMap.get(packageName);
-        } else {
-            notifId = mInstallNotificationId++;
-            mNotifIdMap.put(packageName, notifId);
-        }
-
-        if (mNotificationChannel == null) {
-            mNotificationChannel = new NotificationChannel(WEAR_APPS_CHANNEL,
-                    getString(R.string.wear_app_channel), NotificationManager.IMPORTANCE_MIN);
-            NotificationManager notificationManager = getSystemService(NotificationManager.class);
-            notificationManager.createNotificationChannel(mNotificationChannel);
-        }
-        return new Pair<>(notifId, new Notification.Builder(this, WEAR_APPS_CHANNEL)
-            .setSmallIcon(R.drawable.ic_file_download)
-            .setContentTitle(title)
-            .build());
-    }
-
-    private void getLabelAndUpdateNotification(String packageName, String title) {
-        // Update notification since we have a label now.
-        NotificationManager notificationManager = getSystemService(NotificationManager.class);
-        Pair<Integer, Notification> notifPair = buildNotification(packageName, title);
-        notificationManager.notify(notifPair.first, notifPair.second);
-    }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageUtil.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageUtil.java
deleted file mode 100644
index 6a9145d..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageUtil.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Copyright (C) 2015 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.packageinstaller.wear;
-
-import android.content.Context;
-import android.net.Uri;
-import android.os.ParcelFileDescriptor;
-import android.system.ErrnoException;
-import android.system.Os;
-import android.text.TextUtils;
-import android.util.Log;
-
-import org.tukaani.xz.LZMAInputStream;
-import org.tukaani.xz.XZInputStream;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-
-public class WearPackageUtil {
-    private static final String TAG = "WearablePkgInstaller";
-
-    private static final String COMPRESSION_LZMA = "lzma";
-    private static final String COMPRESSION_XZ = "xz";
-
-    public static File getTemporaryFile(Context context, String packageName) {
-        try {
-            File newFileDir = new File(context.getFilesDir(), "tmp");
-            newFileDir.mkdirs();
-            Os.chmod(newFileDir.getAbsolutePath(), 0771);
-            File newFile = new File(newFileDir, packageName + ".apk");
-            return newFile;
-        }   catch (ErrnoException e) {
-            Log.e(TAG, "Failed to open.", e);
-            return null;
-        }
-    }
-
-    public static File getIconFile(final Context context, final String packageName) {
-        try {
-            File newFileDir = new File(context.getFilesDir(), "images/icons");
-            newFileDir.mkdirs();
-            Os.chmod(newFileDir.getAbsolutePath(), 0771);
-            return new File(newFileDir, packageName + ".icon");
-        }   catch (ErrnoException e) {
-            Log.e(TAG, "Failed to open.", e);
-            return null;
-        }
-    }
-
-    /**
-     * In order to make sure that the Wearable Asset Manager has a reasonable apk that can be used
-     * by the PackageManager, we will parse it before sending it to the PackageManager.
-     * Unfortunately, ParsingPackageUtils needs a file to parse. So, we have to temporarily convert
-     * the fd to a File.
-     *
-     * @param context
-     * @param fd FileDescriptor to convert to File
-     * @param packageName Name of package, will define the name of the file
-     * @param compressionAlg Can be null. For ALT mode the APK will be compressed. We will
-     *                       decompress it here
-     */
-    public static File getFileFromFd(Context context, ParcelFileDescriptor fd,
-            String packageName, String compressionAlg) {
-        File newFile = getTemporaryFile(context, packageName);
-        if (fd == null || fd.getFileDescriptor() == null)  {
-            return null;
-        }
-        InputStream fr = new ParcelFileDescriptor.AutoCloseInputStream(fd);
-        try {
-            if (TextUtils.equals(compressionAlg, COMPRESSION_XZ)) {
-                fr = new XZInputStream(fr);
-            } else if (TextUtils.equals(compressionAlg, COMPRESSION_LZMA)) {
-                fr = new LZMAInputStream(fr);
-            }
-        } catch (IOException e) {
-            Log.e(TAG, "Compression was set to " + compressionAlg + ", but could not decode ", e);
-            return null;
-        }
-
-        int nRead;
-        byte[] data = new byte[1024];
-        try {
-            final FileOutputStream fo = new FileOutputStream(newFile);
-            while ((nRead = fr.read(data, 0, data.length)) != -1) {
-                fo.write(data, 0, nRead);
-            }
-            fo.flush();
-            fo.close();
-            Os.chmod(newFile.getAbsolutePath(), 0644);
-            return newFile;
-        } catch (IOException e) {
-            Log.e(TAG, "Reading from Asset FD or writing to temp file failed ", e);
-            return null;
-        }   catch (ErrnoException e) {
-            Log.e(TAG, "Could not set permissions on file ", e);
-            return null;
-        } finally {
-            try {
-                fr.close();
-            } catch (IOException e) {
-                Log.e(TAG, "Failed to close the file from FD ", e);
-            }
-        }
-    }
-
-    /**
-     * @return com.google.com from expected formats like
-     * Uri: package:com.google.com, package:/com.google.com, package://com.google.com
-     */
-    public static String getSanitizedPackageName(Uri packageUri) {
-        String packageName = packageUri.getEncodedSchemeSpecificPart();
-        if (packageName != null) {
-            return packageName.replaceAll("^/+", "");
-        }
-        return packageName;
-    }
-}
diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp
index 0b36757..d6cbf2a 100644
--- a/packages/SettingsLib/Android.bp
+++ b/packages/SettingsLib/Android.bp
@@ -88,6 +88,7 @@
 aconfig_declarations {
     name: "settingslib_media_flags",
     package: "com.android.settingslib.media.flags",
+    container: "system",
     srcs: [
         "aconfig/settingslib_media_flag_declarations.aconfig",
     ],
@@ -101,6 +102,7 @@
 aconfig_declarations {
     name: "settingslib_flags",
     package: "com.android.settingslib.flags",
+    container: "system",
     srcs: [
         "aconfig/settingslib.aconfig",
     ],
diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig
index 54c5a14..e09ab00 100644
--- a/packages/SettingsLib/aconfig/settingslib.aconfig
+++ b/packages/SettingsLib/aconfig/settingslib.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.settingslib.flags"
+container: "system"
 
 flag {
     name: "new_status_bar_icons"
diff --git a/packages/SettingsLib/aconfig/settingslib_media_flag_declarations.aconfig b/packages/SettingsLib/aconfig/settingslib_media_flag_declarations.aconfig
index f3e537b..4d70aec 100644
--- a/packages/SettingsLib/aconfig/settingslib_media_flag_declarations.aconfig
+++ b/packages/SettingsLib/aconfig/settingslib_media_flag_declarations.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.settingslib.media.flags"
+container: "system"
 
 flag {
   name: "use_media_router2_for_info_media_manager"
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/StorageStatsSource.java b/packages/SettingsLib/src/com/android/settingslib/applications/StorageStatsSource.java
index 6b833cc..0282f03 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/StorageStatsSource.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/StorageStatsSource.java
@@ -16,6 +16,7 @@
 
 package com.android.settingslib.applications;
 
+import android.annotation.NonNull;
 import android.app.usage.StorageStats;
 import android.app.usage.StorageStatsManager;
 import android.content.Context;
@@ -25,6 +26,7 @@
 import androidx.annotation.VisibleForTesting;
 
 import java.io.IOException;
+import java.util.UUID;
 
 /**
  * StorageStatsSource wraps the StorageStatsManager for testability purposes.
@@ -59,6 +61,10 @@
         return mStorageStatsManager.getCacheQuotaBytes(volumeUuid, uid);
     }
 
+    public long getTotalBytes(@NonNull UUID storageUuid) throws IOException {
+        return mStorageStatsManager.getTotalBytes(storageUuid);
+    }
+
     /**
      * Static class that provides methods for querying the amount of external storage available as
      * well as breaking it up into several media types.
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
index e34c50e..4e6d3cb 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
@@ -174,6 +174,7 @@
 
     public void startScan() {
         mMediaDevices.clear();
+        registerRouter();
         startScanOnRouter();
         updateRouteListingPreference();
         refreshDevices();
@@ -188,10 +189,19 @@
         }
     }
 
-    public abstract void stopScan();
+    public final void stopScan() {
+        stopScanOnRouter();
+        unregisterRouter();
+    }
+
+    protected abstract void stopScanOnRouter();
 
     protected abstract void startScanOnRouter();
 
+    protected abstract void registerRouter();
+
+    protected abstract void unregisterRouter();
+
     protected abstract void transferToRoute(@NonNull MediaRoute2Info route);
 
     protected abstract void selectRoute(
@@ -514,17 +524,20 @@
     // MediaRoute2Info.getType was made public on API 34, but exists since API 30.
     @SuppressWarnings("NewApi")
     private synchronized void buildAvailableRoutes() {
-        for (MediaRoute2Info route : getAvailableRoutes()) {
+        RoutingSessionInfo activeSession = getActiveRoutingSession();
+
+        for (MediaRoute2Info route : getAvailableRoutes(activeSession)) {
             if (DEBUG) {
                 Log.d(TAG, "buildAvailableRoutes() route : " + route.getName() + ", volume : "
                         + route.getVolume() + ", type : " + route.getType());
             }
-            addMediaDevice(route);
+            addMediaDevice(route, activeSession);
         }
     }
-    private synchronized List<MediaRoute2Info> getAvailableRoutes() {
+
+    private synchronized List<MediaRoute2Info> getAvailableRoutes(
+            RoutingSessionInfo activeSession) {
         List<MediaRoute2Info> availableRoutes = new ArrayList<>();
-        RoutingSessionInfo activeSession = getActiveRoutingSession();
 
         List<MediaRoute2Info> selectedRoutes = getSelectedRoutes(activeSession);
         availableRoutes.addAll(selectedRoutes);
@@ -562,7 +575,7 @@
     // MediaRoute2Info.getType was made public on API 34, but exists since API 30.
     @SuppressWarnings("NewApi")
     @VisibleForTesting
-    void addMediaDevice(MediaRoute2Info route) {
+    void addMediaDevice(MediaRoute2Info route, RoutingSessionInfo activeSession) {
         final int deviceType = route.getType();
         MediaDevice mediaDevice = null;
         switch (deviceType) {
@@ -627,14 +640,13 @@
                 break;
         }
 
-        if (mediaDevice != null
-                && getActiveRoutingSession().getSelectedRoutes().contains(route.getId())) {
-            mediaDevice.setState(STATE_SELECTED);
-            if (mCurrentConnectedDevice == null) {
-                mCurrentConnectedDevice = mediaDevice;
-            }
-        }
         if (mediaDevice != null) {
+            if (activeSession.getSelectedRoutes().contains(route.getId())) {
+                mediaDevice.setState(STATE_SELECTED);
+                if (mCurrentConnectedDevice == null) {
+                    mCurrentConnectedDevice = mediaDevice;
+                }
+            }
             mMediaDevices.add(mediaDevice);
         }
     }
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java
index c4fac35..23063da 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java
@@ -62,22 +62,30 @@
     @Override
     protected void startScanOnRouter() {
         if (!mIsScanning) {
-            mRouterManager.registerCallback(mExecutor, mMediaRouterCallback);
             mRouterManager.registerScanRequest();
             mIsScanning = true;
         }
     }
 
     @Override
-    public void stopScan() {
+    protected void registerRouter() {
+        mRouterManager.registerCallback(mExecutor, mMediaRouterCallback);
+    }
+
+    @Override
+    protected void stopScanOnRouter() {
         if (mIsScanning) {
-            mRouterManager.unregisterCallback(mMediaRouterCallback);
             mRouterManager.unregisterScanRequest();
             mIsScanning = false;
         }
     }
 
     @Override
+    protected void unregisterRouter() {
+        mRouterManager.unregisterCallback(mMediaRouterCallback);
+    }
+
+    @Override
     protected void transferToRoute(@NonNull MediaRoute2Info route) {
         // TODO: b/279555229 - provide real user handle of a caller.
         mRouterManager.transfer(mPackageName, route, android.os.Process.myUserHandle());
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java
index 2b8c2dd..cf11c6d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java
@@ -63,12 +63,22 @@
     }
 
     @Override
-    public void stopScan() {
+    protected void startScanOnRouter() {
         // Do nothing.
     }
 
     @Override
-    protected void startScanOnRouter() {
+    protected void registerRouter() {
+        // Do nothing.
+    }
+
+    @Override
+    protected void stopScanOnRouter() {
+        // Do nothing.
+    }
+
+    @Override
+    protected void unregisterRouter() {
         // Do nothing.
     }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java
index 9c82cb1..0dceeba 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java
@@ -97,11 +97,6 @@
 
     @Override
     protected void startScanOnRouter() {
-        mRouter.registerRouteCallback(mExecutor, mRouteCallback, RouteDiscoveryPreference.EMPTY);
-        mRouter.registerRouteListingPreferenceUpdatedCallback(
-                mExecutor, mRouteListingPreferenceCallback);
-        mRouter.registerTransferCallback(mExecutor, mTransferCallback);
-        mRouter.registerControllerCallback(mExecutor, mControllerCallback);
         if (Flags.enableScreenOffScanning()) {
             MediaRouter2.ScanRequest request = new MediaRouter2.ScanRequest.Builder().build();
             mScanToken.compareAndSet(null, mRouter.requestScan(request));
@@ -111,7 +106,16 @@
     }
 
     @Override
-    public void stopScan() {
+    protected void registerRouter() {
+        mRouter.registerRouteCallback(mExecutor, mRouteCallback, RouteDiscoveryPreference.EMPTY);
+        mRouter.registerRouteListingPreferenceUpdatedCallback(
+                mExecutor, mRouteListingPreferenceCallback);
+        mRouter.registerTransferCallback(mExecutor, mTransferCallback);
+        mRouter.registerControllerCallback(mExecutor, mControllerCallback);
+    }
+
+    @Override
+    protected void stopScanOnRouter() {
         if (Flags.enableScreenOffScanning()) {
             MediaRouter2.ScanToken token = mScanToken.getAndSet(null);
             if (token != null) {
@@ -120,6 +124,10 @@
         } else {
             mRouter.stopScan();
         }
+    }
+
+    @Override
+    protected void unregisterRouter() {
         mRouter.unregisterControllerCallback(mControllerCallback);
         mRouter.unregisterTransferCallback(mTransferCallback);
         mRouter.unregisterRouteListingPreferenceUpdatedCallback(mRouteListingPreferenceCallback);
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
index d85d253..f8dcec7 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
@@ -86,6 +86,30 @@
     private static final String TEST_DUPLICATED_ID_2 = "test_duplicated_id_2";
     private static final String TEST_DUPLICATED_ID_3 = "test_duplicated_id_3";
 
+    private static final String TEST_SYSTEM_ROUTE_ID = "TEST_SYSTEM_ROUTE_ID";
+    private static final String TEST_BLUETOOTH_ROUTE_ID = "TEST_BT_ROUTE_ID";
+
+    private static final RoutingSessionInfo TEST_SYSTEM_ROUTING_SESSION =
+            new RoutingSessionInfo.Builder("FAKE_SYSTEM_ROUTING_SESSION_ID", TEST_PACKAGE_NAME)
+                    .addSelectedRoute(TEST_SYSTEM_ROUTE_ID)
+                    .addTransferableRoute(TEST_BLUETOOTH_ROUTE_ID)
+                    .setSystemSession(true)
+                    .build();
+
+    private static final MediaRoute2Info TEST_SELECTED_SYSTEM_ROUTE =
+            new MediaRoute2Info.Builder(TEST_SYSTEM_ROUTE_ID, "SELECTED_SYSTEM_ROUTE")
+                    .setSystemRoute(true)
+                    .addFeature(MediaRoute2Info.FEATURE_LIVE_AUDIO)
+                    .build();
+
+    private static final MediaRoute2Info TEST_BLUETOOTH_ROUTE =
+            new MediaRoute2Info.Builder(TEST_BLUETOOTH_ROUTE_ID, "BLUETOOTH_ROUTE")
+                    .setSystemRoute(true)
+                    .addFeature(MediaRoute2Info.FEATURE_LIVE_AUDIO)
+                    .setType(TYPE_BLUETOOTH_A2DP)
+                    .setAddress("00:00:00:00:00:00")
+                    .build();
+
     @Mock
     private MediaRouter2Manager mRouterManager;
     @Mock
@@ -795,19 +819,19 @@
 
         when(route2Info.getType()).thenReturn(TYPE_REMOTE_SPEAKER);
         when(route2Info.getId()).thenReturn(TEST_ID);
-        mInfoMediaManager.addMediaDevice(route2Info);
+        mInfoMediaManager.addMediaDevice(route2Info, TEST_SYSTEM_ROUTING_SESSION);
         assertThat(mInfoMediaManager.mMediaDevices.get(0) instanceof InfoMediaDevice).isTrue();
 
         when(route2Info.getType()).thenReturn(TYPE_USB_DEVICE);
         when(route2Info.getId()).thenReturn(TEST_ID);
         mInfoMediaManager.mMediaDevices.clear();
-        mInfoMediaManager.addMediaDevice(route2Info);
+        mInfoMediaManager.addMediaDevice(route2Info, TEST_SYSTEM_ROUTING_SESSION);
         assertThat(mInfoMediaManager.mMediaDevices.get(0) instanceof PhoneMediaDevice).isTrue();
 
         when(route2Info.getType()).thenReturn(TYPE_WIRED_HEADSET);
         when(route2Info.getId()).thenReturn(TEST_ID);
         mInfoMediaManager.mMediaDevices.clear();
-        mInfoMediaManager.addMediaDevice(route2Info);
+        mInfoMediaManager.addMediaDevice(route2Info, TEST_SYSTEM_ROUTING_SESSION);
         assertThat(mInfoMediaManager.mMediaDevices.get(0) instanceof PhoneMediaDevice).isTrue();
 
         when(route2Info.getType()).thenReturn(TYPE_BLUETOOTH_A2DP);
@@ -818,12 +842,12 @@
         when(cachedBluetoothDeviceManager.findDevice(any(BluetoothDevice.class)))
                 .thenReturn(cachedDevice);
         mInfoMediaManager.mMediaDevices.clear();
-        mInfoMediaManager.addMediaDevice(route2Info);
+        mInfoMediaManager.addMediaDevice(route2Info, TEST_SYSTEM_ROUTING_SESSION);
         assertThat(mInfoMediaManager.mMediaDevices.get(0) instanceof BluetoothMediaDevice).isTrue();
 
         when(route2Info.getType()).thenReturn(TYPE_BUILTIN_SPEAKER);
         mInfoMediaManager.mMediaDevices.clear();
-        mInfoMediaManager.addMediaDevice(route2Info);
+        mInfoMediaManager.addMediaDevice(route2Info, TEST_SYSTEM_ROUTING_SESSION);
         assertThat(mInfoMediaManager.mMediaDevices.get(0) instanceof PhoneMediaDevice).isTrue();
     }
 
@@ -841,26 +865,27 @@
                 .thenReturn(null);
 
         mInfoMediaManager.mMediaDevices.clear();
-        mInfoMediaManager.addMediaDevice(route2Info);
+        mInfoMediaManager.addMediaDevice(route2Info, TEST_SYSTEM_ROUTING_SESSION);
 
         assertThat(mInfoMediaManager.mMediaDevices.size()).isEqualTo(0);
     }
 
     @Test
     public void addMediaDevice_deviceIncludedInSelectedDevices_shouldSetAsCurrentConnected() {
-        final MediaRoute2Info route2Info = mock(MediaRoute2Info.class);
         final CachedBluetoothDeviceManager cachedBluetoothDeviceManager =
                 mock(CachedBluetoothDeviceManager.class);
-        final CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class);
-        final List<RoutingSessionInfo> routingSessionInfos = new ArrayList<>();
-        final RoutingSessionInfo sessionInfo = mock(RoutingSessionInfo.class);
-        routingSessionInfos.add(sessionInfo);
 
-        when(mRouterManager.getRoutingSessions(TEST_PACKAGE_NAME)).thenReturn(routingSessionInfos);
-        when(sessionInfo.getSelectedRoutes()).thenReturn(ImmutableList.of(TEST_ID));
-        when(route2Info.getType()).thenReturn(TYPE_BLUETOOTH_A2DP);
-        when(route2Info.getAddress()).thenReturn("00:00:00:00:00:00");
-        when(route2Info.getId()).thenReturn(TEST_ID);
+        final CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class);
+        RoutingSessionInfo selectedBtSession =
+                new RoutingSessionInfo.Builder(TEST_SYSTEM_ROUTING_SESSION)
+                        .clearSelectedRoutes()
+                        .clearTransferableRoutes()
+                        .addSelectedRoute(TEST_BLUETOOTH_ROUTE_ID)
+                        .addTransferableRoute(TEST_SYSTEM_ROUTE_ID)
+                        .build();
+
+        when(mRouterManager.getRoutingSessions(TEST_PACKAGE_NAME))
+                .thenReturn(List.of(selectedBtSession));
         when(mLocalBluetoothManager.getCachedDeviceManager())
                 .thenReturn(cachedBluetoothDeviceManager);
         when(cachedBluetoothDeviceManager.findDevice(any(BluetoothDevice.class)))
@@ -868,7 +893,7 @@
         mInfoMediaManager.mRouterManager = mRouterManager;
 
         mInfoMediaManager.mMediaDevices.clear();
-        mInfoMediaManager.addMediaDevice(route2Info);
+        mInfoMediaManager.addMediaDevice(TEST_BLUETOOTH_ROUTE, selectedBtSession);
 
         MediaDevice device = mInfoMediaManager.mMediaDevices.get(0);
 
diff --git a/packages/SettingsProvider/Android.bp b/packages/SettingsProvider/Android.bp
index bf4f60d..e9c2672 100644
--- a/packages/SettingsProvider/Android.bp
+++ b/packages/SettingsProvider/Android.bp
@@ -32,6 +32,7 @@
         "unsupportedappusage",
     ],
     static_libs: [
+        "aconfig_demo_flags_java_lib",
         "device_config_service_flags_java",
         "libaconfig_java_proto_lite",
         "SettingsLibDeviceStateRotationLock",
@@ -87,6 +88,7 @@
 aconfig_declarations {
     name: "device_config_service_flags",
     package: "com.android.providers.settings",
+    container: "system",
     srcs: [
         "src/com/android/providers/settings/device_config_service.aconfig",
     ],
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
index 4e4c22f..68167e1 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
@@ -60,16 +60,17 @@
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
-import java.io.BufferedWriter;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
-import java.io.FileWriter;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.PosixFileAttributes;
+import java.nio.file.attribute.PosixFilePermission;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -165,8 +166,8 @@
 
     private static final String STORAGE_MIGRATION_FLAG =
             "core_experiments_team_internal/com.android.providers.settings.storage_test_mission_1";
-    private static final String STORAGE_MIGRATION_LOG =
-            "/metadata/aconfig/flags/storage_migration.log";
+    private static final String STORAGE_MIGRATION_MARKER_FILE =
+            "/metadata/aconfig/storage_test_mission_1";
 
     /**
      * This tag is applied to all aconfig default value-loaded flags.
@@ -1126,7 +1127,7 @@
                     Slog.i(LOG_TAG, "[PERSIST END]");
                 }
             } catch (Throwable t) {
-                Slog.wtf(LOG_TAG, "Failed to write settings, restoring old file", t);
+                Slog.e(LOG_TAG, "Failed to write settings, restoring old file", t);
                 if (t instanceof IOException) {
                     if (t.getMessage().contains("Couldn't create directory")) {
                         if (DEBUG) {
@@ -1467,16 +1468,29 @@
                     }
                 }
 
-                if (name != null && name.equals(STORAGE_MIGRATION_FLAG) && value.equals("true")) {
-                    File file = new File(STORAGE_MIGRATION_LOG);
-                    if (!file.exists()) {
-                        try (BufferedWriter writer =
-                                new BufferedWriter(new FileWriter(STORAGE_MIGRATION_LOG))) {
-                            final long timestamp = System.currentTimeMillis();
-                            String entry = String.format("%d | Log init", timestamp);
-                            writer.write(entry);
-                        } catch (IOException e) {
-                            Slog.e(LOG_TAG, "failed to write storage migration file", e);
+                if (isConfigSettingsKey(mKey) && name != null
+                        && name.equals(STORAGE_MIGRATION_FLAG)) {
+                    if (value.equals("true")) {
+                        Path path = Paths.get(STORAGE_MIGRATION_MARKER_FILE);
+                        if (!Files.exists(path)) {
+                            Files.createFile(path);
+                        }
+
+                        Set<PosixFilePermission> perms =
+                                Files.readAttributes(path, PosixFileAttributes.class).permissions();
+                        perms.add(PosixFilePermission.OWNER_WRITE);
+                        perms.add(PosixFilePermission.OWNER_READ);
+                        perms.add(PosixFilePermission.GROUP_READ);
+                        perms.add(PosixFilePermission.OTHERS_READ);
+                        try {
+                            Files.setPosixFilePermissions(path, perms);
+                        } catch (Exception e) {
+                            Slog.e(LOG_TAG, "failed to set permissions on migration marker", e);
+                        }
+                    } else {
+                        java.nio.file.Path path = Paths.get(STORAGE_MIGRATION_MARKER_FILE);
+                        if (Files.exists(path)) {
+                            Files.delete(path);
                         }
                     }
                 }
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig
index c572bdb..d20fbf5 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig
+++ b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.providers.settings"
+container: "system"
 
 flag {
     name: "support_overrides"
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 5804071..b94e224 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -896,7 +896,6 @@
 
     <!-- Permissions required for CTS test - CtsVoiceInteractionTestCases -->
     <uses-permission android:name="android.permission.RESET_HOTWORD_TRAINING_DATA_EGRESS_COUNT" />
-    <uses-permission android:name="android.permission.RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA" />
     <uses-permission android:name="android.permission.RECEIVE_SANDBOX_TRIGGER_AUDIO" />
 
     <uses-permission android:name="android.permission.GET_BINDING_UID_IMPORTANCE" />
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/aconfig/Android.bp b/packages/SystemUI/accessibility/accessibilitymenu/aconfig/Android.bp
index f74e59a..0ff856e 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/aconfig/Android.bp
+++ b/packages/SystemUI/accessibility/accessibilitymenu/aconfig/Android.bp
@@ -5,6 +5,7 @@
 aconfig_declarations {
     name: "com_android_a11y_menu_flags",
     package: "com.android.systemui.accessibility.accessibilitymenu",
+    container: "system",
     srcs: [
         "accessibility.aconfig",
     ],
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig b/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig
index f5db6a4..d868d5c 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig
+++ b/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.systemui.accessibility.accessibilitymenu"
+container: "system"
 
 # NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors.
 
diff --git a/packages/SystemUI/aconfig/Android.bp b/packages/SystemUI/aconfig/Android.bp
index 15c2c17..2a32b58 100644
--- a/packages/SystemUI/aconfig/Android.bp
+++ b/packages/SystemUI/aconfig/Android.bp
@@ -36,6 +36,7 @@
 aconfig_declarations {
     name: "com_android_systemui_flags",
     package: "com.android.systemui",
+    container: "system",
     srcs: [
         "*.aconfig",
     ],
diff --git a/packages/SystemUI/aconfig/accessibility.aconfig b/packages/SystemUI/aconfig/accessibility.aconfig
index 866aa89..8137e40 100644
--- a/packages/SystemUI/aconfig/accessibility.aconfig
+++ b/packages/SystemUI/aconfig/accessibility.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.systemui"
+container: "system"
 
 # NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors.
 
diff --git a/packages/SystemUI/aconfig/biometrics_framework.aconfig b/packages/SystemUI/aconfig/biometrics_framework.aconfig
index 7cc0c83..bd1a442 100644
--- a/packages/SystemUI/aconfig/biometrics_framework.aconfig
+++ b/packages/SystemUI/aconfig/biometrics_framework.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.systemui"
+container: "system"
 
 # NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors.
 
@@ -14,4 +15,4 @@
     namespace: "biometrics_framework"
     description: "Refactors Biometric Prompt to use a ConstraintLayout"
     bug: "288175072"
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/aconfig/communal.aconfig b/packages/SystemUI/aconfig/communal.aconfig
index 2c6ff97..2e9af7e 100644
--- a/packages/SystemUI/aconfig/communal.aconfig
+++ b/packages/SystemUI/aconfig/communal.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.systemui"
+container: "system"
 
 flag {
     name: "communal_hub"
diff --git a/packages/SystemUI/aconfig/cross_device_control.aconfig b/packages/SystemUI/aconfig/cross_device_control.aconfig
index d3f14c1..5f9a4f4 100644
--- a/packages/SystemUI/aconfig/cross_device_control.aconfig
+++ b/packages/SystemUI/aconfig/cross_device_control.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.systemui"
+container: "system"
 
 flag {
     name: "legacy_le_audio_sharing"
diff --git a/packages/SystemUI/aconfig/predictive_back.aconfig b/packages/SystemUI/aconfig/predictive_back.aconfig
index 7bbe82c..46eb9e1 100644
--- a/packages/SystemUI/aconfig/predictive_back.aconfig
+++ b/packages/SystemUI/aconfig/predictive_back.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.systemui"
+container: "system"
 
 flag {
     name: "predictive_back_sysui"
@@ -26,4 +27,4 @@
     namespace: "systemui"
     description: "Enable Predictive Back Animation for SysUI dialogs"
     bug: "327721544"
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 4bfc629..112d964 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.systemui"
+container: "system"
 
 flag {
     name: "example_flag"
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt
index 7a73c58..8129e41 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt
@@ -71,7 +71,6 @@
 
     return remember(clock, topInset, topmostTop) {
         BurnInParameters(
-            clockControllerProvider = { clock },
             topInset = topInset,
             minViewY = topmostTop,
         )
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 fa0a1c4..a31b533 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
@@ -33,7 +33,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
-import com.android.systemui.notifications.ui.composable.NotificationStack
+import com.android.systemui.notifications.ui.composable.ConstrainedNotificationStack
 import com.android.systemui.res.R
 import com.android.systemui.shade.LargeScreenHeaderHelper
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
@@ -94,7 +94,7 @@
             return
         }
 
-        NotificationStack(
+        ConstrainedNotificationStack(
             viewModel = viewModel,
             modifier =
                 modifier.fillMaxWidth().thenIf(shouldUseSplitNotificationShade) {
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 cda4347..579e837 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
@@ -36,12 +36,14 @@
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.draw.drawBehind
@@ -60,7 +62,6 @@
 import androidx.compose.ui.res.dimensionResource
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.util.lerp
 import com.android.compose.animation.scene.ElementKey
@@ -68,12 +69,12 @@
 import com.android.compose.animation.scene.SceneScope
 import com.android.compose.modifiers.height
 import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius
-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.ShadeScrimBounds
 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimRounding
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
 import kotlin.math.roundToInt
@@ -81,7 +82,8 @@
 object Notifications {
     object Elements {
         val NotificationScrim = ElementKey("NotificationScrim")
-        val NotificationPlaceholder = ElementKey("NotificationPlaceholder")
+        val NotificationStackPlaceholder = ElementKey("NotificationStackPlaceholder")
+        val HeadsUpNotificationPlaceholder = ElementKey("HeadsUpNotificationPlaceholder")
         val ShelfSpace = ElementKey("ShelfSpace")
     }
 
@@ -91,12 +93,6 @@
         const val EXPANSION_FOR_MAX_CORNER_RADIUS = 0.1f
         const val EXPANSION_FOR_MAX_SCRIM_ALPHA = 0.3f
     }
-
-    enum class Form {
-        HunFromTop,
-        Stack,
-        HunFromBottom,
-    }
 }
 
 /**
@@ -109,24 +105,48 @@
     modifier: Modifier = Modifier,
     isPeekFromBottom: Boolean = false,
 ) {
-    NotificationPlaceholder(
-        viewModel = viewModel,
-        form = if (isPeekFromBottom) Form.HunFromBottom else Form.HunFromTop,
-        modifier = modifier,
-    )
+    val headsUpHeight = viewModel.headsUpHeight.collectAsState()
+
+    Element(
+        Notifications.Elements.HeadsUpNotificationPlaceholder,
+        modifier =
+            modifier
+                .height { headsUpHeight.value.roundToInt() }
+                .fillMaxWidth()
+                .debugBackground(viewModel, DEBUG_HUN_COLOR)
+                .onGloballyPositioned { coordinates: LayoutCoordinates ->
+                    val boundsInWindow = coordinates.boundsInWindow()
+                    debugLog(viewModel) {
+                        "HUNS onGloballyPositioned:" +
+                            " size=${coordinates.size}" +
+                            " bounds=$boundsInWindow"
+                    }
+                    viewModel.onHeadsUpTopChanged(boundsInWindow.top)
+                }
+    ) {
+        content {}
+    }
 }
 
 /** Adds the space where notification stack should appear in the scene. */
 @Composable
-fun SceneScope.NotificationStack(
+fun SceneScope.ConstrainedNotificationStack(
     viewModel: NotificationsPlaceholderViewModel,
     modifier: Modifier = Modifier,
 ) {
-    NotificationPlaceholder(
-        viewModel = viewModel,
-        form = Form.Stack,
-        modifier = modifier,
-    )
+    Box(
+        modifier =
+            modifier.onSizeChanged { viewModel.onConstrainedAvailableSpaceChanged(it.height) }
+    ) {
+        NotificationPlaceholder(
+            viewModel = viewModel,
+            modifier = Modifier.fillMaxSize(),
+        )
+        HeadsUpNotificationSpace(
+            viewModel = viewModel,
+            modifier = Modifier.align(Alignment.TopCenter),
+        )
+    }
 }
 
 /**
@@ -169,6 +189,9 @@
     // entire height of the scrim is visible on screen.
     val scrimOffset = remember { mutableStateOf(0f) }
 
+    // set the bounds to null when the scrim disappears
+    DisposableEffect(Unit) { onDispose { viewModel.onScrimBoundsChanged(null) } }
+
     val minScrimTop = with(density) { ShadeHeader.Dimensions.CollapsedHeight.toPx() }
 
     // The minimum offset for the scrim. The scrim is considered fully expanded when it
@@ -235,6 +258,22 @@
                             .let { scrimRounding.value.toRoundedCornerShape(it) }
                     clip = true
                 }
+                .onGloballyPositioned { coordinates ->
+                    val boundsInWindow = coordinates.boundsInWindow()
+                    debugLog(viewModel) {
+                        "SCRIM onGloballyPositioned:" +
+                            " size=${coordinates.size}" +
+                            " bounds=$boundsInWindow"
+                    }
+                    viewModel.onScrimBoundsChanged(
+                        ShadeScrimBounds(
+                            left = boundsInWindow.left,
+                            top = boundsInWindow.top,
+                            right = boundsInWindow.right,
+                            bottom = boundsInWindow.bottom,
+                        )
+                    )
+                }
     ) {
         // Creates a cutout in the background scrim in the shape of the notifications scrim.
         // Only visible when notif scrim alpha < 1, during shade expansion.
@@ -254,11 +293,10 @@
                             } else 1f
                     }
                     .background(MaterialTheme.colorScheme.surface)
-                    .debugBackground(viewModel, Color(0.5f, 0.5f, 0f, 0.2f))
+                    .debugBackground(viewModel, DEBUG_BOX_COLOR)
         ) {
             NotificationPlaceholder(
                 viewModel = viewModel,
-                form = Form.Stack,
                 modifier =
                     Modifier.verticalNestedScrollToScene(
                             topBehavior = NestedScrollBehavior.EdgeWithPreview,
@@ -284,6 +322,7 @@
                         .height { (stackHeight.value + navBarHeight).roundToInt() },
             )
         }
+        HeadsUpNotificationSpace(viewModel = viewModel)
     }
 }
 
@@ -304,14 +343,10 @@
         modifier
             .element(key = Notifications.Elements.ShelfSpace)
             .fillMaxWidth()
-            .onSizeChanged { size: IntSize ->
-                debugLog(viewModel) { "SHELF onSizeChanged: size=$size" }
-            }
             .onPlaced { coordinates: LayoutCoordinates ->
                 debugLog(viewModel) {
                     ("SHELF onPlaced:" +
                         " size=${coordinates.size}" +
-                        " position=${coordinates.positionInWindow()}" +
                         " bounds=${coordinates.boundsInWindow()}")
                 }
             }
@@ -326,32 +361,26 @@
 @Composable
 private fun SceneScope.NotificationPlaceholder(
     viewModel: NotificationsPlaceholderViewModel,
-    form: Form,
     modifier: Modifier = Modifier,
 ) {
-    val elementKey = Notifications.Elements.NotificationPlaceholder
     Element(
-        elementKey,
+        Notifications.Elements.NotificationStackPlaceholder,
         modifier =
             modifier
-                .debugBackground(viewModel)
-                .onSizeChanged { size: IntSize ->
-                    debugLog(viewModel) { "STACK onSizeChanged: size=$size" }
-                }
+                .debugBackground(viewModel, DEBUG_STACK_COLOR)
+                .onSizeChanged { size -> debugLog(viewModel) { "STACK onSizeChanged: size=$size" } }
                 .onGloballyPositioned { coordinates: LayoutCoordinates ->
-                    viewModel.onContentTopChanged(coordinates.positionInWindow().y)
+                    val positionInWindow = coordinates.positionInWindow()
                     debugLog(viewModel) {
                         "STACK onGloballyPositioned:" +
                             " size=${coordinates.size}" +
-                            " position=${coordinates.positionInWindow()}" +
+                            " position=$positionInWindow" +
                             " bounds=${coordinates.boundsInWindow()}"
                     }
-                    val boundsInWindow = coordinates.boundsInWindow()
-                    viewModel.onBoundsChanged(
-                        left = boundsInWindow.left,
-                        top = boundsInWindow.top,
-                        right = boundsInWindow.right,
-                        bottom = boundsInWindow.bottom,
+                    // NOTE: positionInWindow.y scrolls off screen, but boundsInWindow.top will not
+                    viewModel.onStackBoundsChanged(
+                        top = positionInWindow.y,
+                        bottom = positionInWindow.y + coordinates.size.height,
                     )
                 }
     ) {
@@ -388,7 +417,7 @@
 
 private fun Modifier.debugBackground(
     viewModel: NotificationsPlaceholderViewModel,
-    color: Color = DEBUG_COLOR,
+    color: Color,
 ): Modifier =
     if (viewModel.isVisualDebuggingEnabled) {
         background(color)
@@ -397,8 +426,8 @@
     }
 
 fun ShadeScrimRounding.toRoundedCornerShape(radius: Dp): RoundedCornerShape {
-    val topRadius = if (roundTop) radius else 0.dp
-    val bottomRadius = if (roundBottom) radius else 0.dp
+    val topRadius = if (isTopRounded) radius else 0.dp
+    val bottomRadius = if (isBottomRounded) radius else 0.dp
     return RoundedCornerShape(
         topStart = topRadius,
         topEnd = topRadius,
@@ -408,4 +437,6 @@
 }
 
 private const val TAG = "FlexiNotifs"
-private val DEBUG_COLOR = Color(1f, 0f, 0f, 0.2f)
+private val DEBUG_STACK_COLOR = Color(1f, 0f, 0f, 0.2f)
+private val DEBUG_HUN_COLOR = Color(0f, 0f, 1f, 0.2f)
+private val DEBUG_BOX_COLOR = Color(0f, 1f, 0f, 0.2f)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index 5b9213a..244f480 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.qs.ui.composable
 
 import android.view.ViewGroup
+import androidx.activity.compose.BackHandler
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.core.tween
 import androidx.compose.animation.expandVertically
@@ -122,6 +123,13 @@
     // TODO(b/280887232): implement the real UI.
     Box(modifier = modifier.fillMaxSize()) {
         val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState()
+
+        BackHandler(
+            enabled = isCustomizing,
+        ) {
+            viewModel.qsSceneAdapter.requestCloseCustomizer()
+        }
+
         val collapsedHeaderHeight =
             with(LocalDensity.current) { ShadeHeader.Dimensions.CollapsedHeight.roundToPx() }
         val lifecycleOwner = LocalLifecycleOwner.current
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
index cb8cebf..81878aa 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
@@ -179,10 +179,14 @@
                 setAutoConfirmFeatureEnabled(true)
             }
             assertThat(isAutoConfirmEnabled).isTrue()
-            val shorterPin =
-                FakeAuthenticationRepository.DEFAULT_PIN.toMutableList().apply { removeLast() }
+            val defaultPin = FakeAuthenticationRepository.DEFAULT_PIN.toMutableList()
 
-            assertSkipped(underTest.authenticate(shorterPin, tryAutoConfirm = true))
+            assertSkipped(
+                underTest.authenticate(
+                    defaultPin.subList(0, defaultPin.size - 1),
+                    tryAutoConfirm = true
+                )
+            )
             assertThat(underTest.lockoutEndTimestamp).isNull()
             assertThat(kosmos.fakeAuthenticationRepository.lockoutStartedReportCount).isEqualTo(0)
         }
@@ -201,7 +205,6 @@
 
             assertFailed(
                 underTest.authenticate(wrongPin, tryAutoConfirm = true),
-                assertNoResultEvents = true,
             )
         }
 
@@ -219,7 +222,6 @@
 
             assertFailed(
                 underTest.authenticate(longerPin, tryAutoConfirm = true),
-                assertNoResultEvents = true,
             )
         }
 
@@ -547,14 +549,9 @@
 
     private fun assertFailed(
         authenticationResult: AuthenticationResult,
-        assertNoResultEvents: Boolean = false,
     ) {
         assertThat(authenticationResult).isEqualTo(AuthenticationResult.FAILED)
-        if (assertNoResultEvents) {
-            assertThat(onAuthenticationResult).isNull()
-        } else {
-            assertThat(onAuthenticationResult).isFalse()
-        }
+        assertThat(onAuthenticationResult).isFalse()
     }
 
     private fun assertSkipped(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index a0db482..5bb36a0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -252,7 +252,14 @@
     @Test
     fun onAutoConfirm_whenCorrect() =
         testScope.runTest {
+            // TODO(b/332768183) remove this after the bug if fixed.
+            // Collect the flow so that it is hot, in the real application this is done by using a
+            // refreshingFlow that relies on the UI to make this flow hot.
+            val autoConfirmEnabled by
+                collectLastValue(authenticationInteractor.isAutoConfirmEnabled)
             kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true)
+
+            assertThat(autoConfirmEnabled).isTrue()
             val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
             lockDeviceAndOpenPinBouncer()
 
@@ -264,9 +271,17 @@
     @Test
     fun onAutoConfirm_whenWrong() =
         testScope.runTest {
+            // TODO(b/332768183) remove this after the bug if fixed.
+            // Collect the flow so that it is hot, in the real application this is done by using a
+            // refreshingFlow that relies on the UI to make this flow hot.
+            val autoConfirmEnabled by
+                collectLastValue(authenticationInteractor.isAutoConfirmEnabled)
+
             val currentScene by collectLastValue(sceneInteractor.currentScene)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
             kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true)
+
+            assertThat(autoConfirmEnabled).isTrue()
             lockDeviceAndOpenPinBouncer()
 
             FakeAuthenticationRepository.DEFAULT_PIN.dropLast(1).forEach { digit ->
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
index f517cec..31337a6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.Flags as AConfigFlags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
 import com.android.systemui.keyguard.domain.interactor.burnInInteractor
@@ -60,10 +61,7 @@
     private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
     private lateinit var underTest: AodBurnInViewModel
 
-    private var burnInParameters =
-        BurnInParameters(
-            clockControllerProvider = { clockController },
-        )
+    private var burnInParameters = BurnInParameters()
     private val burnInFlow = MutableStateFlow(BurnInModel())
 
     @Before
@@ -76,6 +74,7 @@
         whenever(goneToAodTransitionViewModel.enterFromTopTranslationY(anyInt()))
             .thenReturn(emptyFlow())
         kosmos.goneToAodTransitionViewModel = goneToAodTransitionViewModel
+        kosmos.fakeKeyguardClockRepository.setCurrentClock(clockController)
 
         underTest = kosmos.aodBurnInViewModel
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
index 27c4ec1..1a9ad3c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
@@ -496,4 +496,16 @@
             runCurrent()
             verify(qsImpl!!).setInSplitShade(true)
         }
+
+    @Test
+    fun requestCloseCustomizer() =
+        testScope.runTest {
+            val qsImpl by collectLastValue(underTest.qsImpl)
+
+            underTest.inflate(context)
+            runCurrent()
+
+            underTest.requestCloseCustomizer()
+            verify(qsImpl!!).closeCustomizer()
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
index ef38567..d9ab3b1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
@@ -133,18 +133,13 @@
         }
 
     @Test
-    fun destinationsCustomizing() =
+    fun destinationsCustomizing_noDestinations() =
         testScope.runTest {
             overrideResource(R.bool.config_use_split_notification_shade, false)
             val destinations by collectLastValue(underTest.destinationScenes)
             qsFlexiglassAdapter.setCustomizing(true)
 
-            assertThat(destinations)
-                .isEqualTo(
-                    mapOf(
-                        Back to UserActionResult(Scenes.QuickSettings),
-                    )
-                )
+            assertThat(destinations).isEmpty()
         }
 
     @Test
@@ -164,18 +159,13 @@
         }
 
     @Test
-    fun destinations_whenCustomizing_inSplitShade() =
+    fun destinations_whenCustomizing_inSplitShade_noDestinations() =
         testScope.runTest {
             overrideResource(R.bool.config_use_split_notification_shade, true)
             val destinations by collectLastValue(underTest.destinationScenes)
             qsFlexiglassAdapter.setCustomizing(true)
 
-            assertThat(destinations)
-                .isEqualTo(
-                    mapOf(
-                        Back to UserActionResult(Scenes.QuickSettings),
-                    )
-                )
+            assertThat(destinations).isEmpty()
         }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt
index 05f4f3c..cbbcce9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt
@@ -189,6 +189,7 @@
 
     private fun TestScope.setDeviceEntered(isEntered: Boolean) {
         if (isEntered) {
+            // Unlock the device marking the device has entered.
             kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
                 SuccessFingerprintAuthenticationStatus(0, true)
             )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
index 53522e2..3c28c0e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
@@ -31,7 +31,9 @@
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.fakeSceneDataSource
 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds
-import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationStackAppearanceViewModel
+import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape
+import com.android.systemui.statusbar.notification.stack.shared.model.ViewPosition
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationScrollViewModel
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
@@ -57,27 +59,44 @@
         }
     private val testScope = kosmos.testScope
     private val placeholderViewModel by lazy { kosmos.notificationsPlaceholderViewModel }
-    private val appearanceViewModel by lazy { kosmos.notificationStackAppearanceViewModel }
+    private val appearanceViewModel by lazy { kosmos.notificationScrollViewModel }
     private val sceneInteractor by lazy { kosmos.sceneInteractor }
     private val fakeSceneDataSource by lazy { kosmos.fakeSceneDataSource }
 
     @Test
     fun updateBounds() =
         testScope.runTest {
-            val clipping by collectLastValue(appearanceViewModel.shadeScrimClipping)
+            val radius = MutableStateFlow(32)
+            val viewPosition = MutableStateFlow(ViewPosition(0, 0))
+            val shape by collectLastValue(appearanceViewModel.shadeScrimShape(radius, viewPosition))
 
-            val top = 200f
-            val left = 0f
-            val bottom = 550f
-            val right = 100f
-            placeholderViewModel.onBoundsChanged(
-                left = left,
-                top = top,
-                right = right,
-                bottom = bottom
+            placeholderViewModel.onScrimBoundsChanged(
+                ShadeScrimBounds(left = 0f, top = 200f, right = 100f, bottom = 550f)
             )
-            assertThat(clipping?.bounds)
-                .isEqualTo(ShadeScrimBounds(left = left, top = top, right = right, bottom = bottom))
+            assertThat(shape)
+                .isEqualTo(
+                    ShadeScrimShape(
+                        bounds =
+                            ShadeScrimBounds(left = 0f, top = 200f, right = 100f, bottom = 550f),
+                        topRadius = 32,
+                        bottomRadius = 0
+                    )
+                )
+
+            viewPosition.value = ViewPosition(200, 15)
+            radius.value = 24
+            placeholderViewModel.onScrimBoundsChanged(
+                ShadeScrimBounds(left = 210f, top = 200f, right = 300f, bottom = 550f)
+            )
+            assertThat(shape)
+                .isEqualTo(
+                    ShadeScrimShape(
+                        bounds =
+                            ShadeScrimBounds(left = 10f, top = 185f, right = 100f, bottom = 535f),
+                        topRadius = 24,
+                        bottomRadius = 0
+                    )
+                )
         }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt
index dc928c4..50b77dc 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
@@ -68,11 +68,11 @@
 
             kosmos.shadeRepository.setShadeMode(ShadeMode.Single)
             assertThat(stackRounding)
-                .isEqualTo(ShadeScrimRounding(roundTop = true, roundBottom = false))
+                .isEqualTo(ShadeScrimRounding(isTopRounded = true, isBottomRounded = false))
 
             kosmos.shadeRepository.setShadeMode(ShadeMode.Split)
             assertThat(stackRounding)
-                .isEqualTo(ShadeScrimRounding(roundTop = true, roundBottom = true))
+                .isEqualTo(ShadeScrimRounding(isTopRounded = true, isBottomRounded = true))
         }
 
     @Test(expected = IllegalStateException::class)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt
index d4a7049..1f0812d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt
@@ -16,14 +16,10 @@
 
 package com.android.systemui.statusbar.notification.stack.ui.viewmodel
 
-import android.platform.test.annotations.DisableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.shared.model.NotificationContainerBounds
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor
 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds
@@ -40,23 +36,21 @@
     private val underTest = kosmos.notificationsPlaceholderViewModel
 
     @Test
-    @DisableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
-    fun onBoundsChanged_setsNotificationContainerBounds() =
+    fun onBoundsChanged() =
         kosmos.testScope.runTest {
-            underTest.onBoundsChanged(left = 5f, top = 5f, right = 5f, bottom = 5f)
-            val containerBounds by
-                collectLastValue(kosmos.keyguardInteractor.notificationContainerBounds)
+            val bounds = ShadeScrimBounds(left = 5f, top = 15f, right = 25f, bottom = 35f)
+            underTest.onScrimBoundsChanged(bounds)
             val stackBounds by
                 collectLastValue(kosmos.notificationStackAppearanceInteractor.shadeScrimBounds)
-            assertThat(containerBounds)
-                .isEqualTo(NotificationContainerBounds(top = 5f, bottom = 5f))
-            assertThat(stackBounds)
-                .isEqualTo(ShadeScrimBounds(left = 5f, top = 5f, right = 5f, bottom = 5f))
+            assertThat(stackBounds).isEqualTo(bounds)
         }
 
     @Test
-    fun onContentTopChanged_setsContentTop() {
-        underTest.onContentTopChanged(padding = 5f)
-        assertThat(kosmos.notificationStackAppearanceInteractor.stackTop.value).isEqualTo(5f)
-    }
+    fun onStackBoundsChanged() =
+        kosmos.testScope.runTest {
+            underTest.onStackBoundsChanged(top = 5f, bottom = 500f)
+            assertThat(kosmos.notificationStackAppearanceInteractor.stackTop.value).isEqualTo(5f)
+            assertThat(kosmos.notificationStackAppearanceInteractor.stackBottom.value)
+                .isEqualTo(500f)
+        }
 }
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml b/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml
index 173d57b..3b6b5a0 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml
@@ -91,6 +91,7 @@
                 android:layout_height="wrap_content"
                 android:contentDescription="@string/keyguard_accessibility_password"
                 android:gravity="center_horizontal"
+                android:layout_gravity="center"
                 android:imeOptions="flagForceAscii|actionDone"
                 android:inputType="textPassword"
                 android:maxLength="500"
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml
index 909d4fc..5aac653 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml
@@ -55,6 +55,7 @@
              android:layout_height="wrap_content"
              android:contentDescription="@string/keyguard_accessibility_password"
              android:gravity="center"
+             android:layout_gravity="center"
              android:singleLine="true"
              android:textStyle="normal"
              android:inputType="textPassword"
diff --git a/packages/SystemUI/res/anim/slide_in_up.xml b/packages/SystemUI/res/anim/slide_in_up.xml
new file mode 100644
index 0000000..6089a28
--- /dev/null
+++ b/packages/SystemUI/res/anim/slide_in_up.xml
@@ -0,0 +1,21 @@
+<?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.
+-->
+
+<translate
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:fromYDelta="100%p"
+    android:toYDelta="0"
+    android:duration="@android:integer/config_shortAnimTime" />
diff --git a/packages/SystemUI/res/anim/slide_out_down.xml b/packages/SystemUI/res/anim/slide_out_down.xml
new file mode 100644
index 0000000..5a7b591
--- /dev/null
+++ b/packages/SystemUI/res/anim/slide_out_down.xml
@@ -0,0 +1,21 @@
+<?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.
+-->
+
+<translate
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:fromYDelta="0"
+    android:toYDelta="100%p"
+    android:duration="@android:integer/config_shortAnimTime" />
diff --git a/packages/SystemUI/res/drawable/ic_shortcutlist_search.xml b/packages/SystemUI/res/drawable/ic_shortcutlist_search.xml
index 1b12e74..0406f0e 100644
--- a/packages/SystemUI/res/drawable/ic_shortcutlist_search.xml
+++ b/packages/SystemUI/res/drawable/ic_shortcutlist_search.xml
@@ -14,12 +14,15 @@
   limitations under the License
   -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
         android:width="24dp"
         android:height="24dp"
         android:viewportWidth="48"
         android:viewportHeight="48"
-        android:tint="?android:attr/textColorSecondary">
+        android:tint="?androidprv:attr/materialColorOnSurfaceVariant">
     <path
         android:fillColor="@android:color/white"
+        android:strokeColor="@android:color/white"
+        android:strokeWidth="2"
         android:pathData="M39.8,41.95 L26.65,28.8Q25.15,30.1 23.15,30.825Q21.15,31.55 18.9,31.55Q13.5,31.55 9.75,27.8Q6,24.05 6,18.75Q6,13.45 9.75,9.7Q13.5,5.95 18.85,5.95Q24.15,5.95 27.875,9.7Q31.6,13.45 31.6,18.75Q31.6,20.9 30.9,22.9Q30.2,24.9 28.8,26.65L42,39.75ZM18.85,28.55Q22.9,28.55 25.75,25.675Q28.6,22.8 28.6,18.75Q28.6,14.7 25.75,11.825Q22.9,8.95 18.85,8.95Q14.75,8.95 11.875,11.825Q9,14.7 9,18.75Q9,22.8 11.875,25.675Q14.75,28.55 18.85,28.55Z"/>
-</vector>
\ No newline at end of file
+</vector>
diff --git a/packages/SystemUI/res/drawable/shortcut_button_colored.xml b/packages/SystemUI/res/drawable/shortcut_button_colored.xml
index bf90853..2e2d9b9 100644
--- a/packages/SystemUI/res/drawable/shortcut_button_colored.xml
+++ b/packages/SystemUI/res/drawable/shortcut_button_colored.xml
@@ -21,7 +21,7 @@
         android:color="?android:attr/colorControlHighlight">
         <item>
             <shape android:shape="rectangle">
-                <corners android:radius="16dp"/>
+              <corners android:radius="@dimen/ksh_button_corner_radius"/>
                 <solid android:color="?androidprv:attr/materialColorSurfaceBright"/>
             </shape>
         </item>
diff --git a/packages/SystemUI/res/drawable/shortcut_button_focus_colored.xml b/packages/SystemUI/res/drawable/shortcut_button_focus_colored.xml
index f692ed97..5b88bb9 100644
--- a/packages/SystemUI/res/drawable/shortcut_button_focus_colored.xml
+++ b/packages/SystemUI/res/drawable/shortcut_button_focus_colored.xml
@@ -21,7 +21,7 @@
         android:color="?android:attr/colorControlHighlight">
         <item>
             <shape android:shape="rectangle">
-                <corners android:radius="16dp"/>
+              <corners android:radius="@dimen/ksh_button_corner_radius"/>
                 <solid android:color="?androidprv:attr/materialColorPrimary"/>
             </shape>
         </item>
diff --git a/packages/SystemUI/res/drawable/shortcut_dialog_bg.xml b/packages/SystemUI/res/drawable/shortcut_dialog_bg.xml
index 6ce3eae..aa0b268 100644
--- a/packages/SystemUI/res/drawable/shortcut_dialog_bg.xml
+++ b/packages/SystemUI/res/drawable/shortcut_dialog_bg.xml
@@ -17,8 +17,8 @@
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
     <solid android:color="?android:attr/colorBackground"/>
-    <corners android:topLeftRadius="16dp"
-             android:topRightRadius="16dp"
+    <corners android:topLeftRadius="@dimen/ksh_dialog_top_corner_radius"
+             android:topRightRadius="@dimen/ksh_dialog_top_corner_radius"
              android:bottomLeftRadius="0dp"
              android:bottomRightRadius="0dp"/>
-</shape>
\ No newline at end of file
+</shape>
diff --git a/packages/SystemUI/res/drawable/shortcut_search_background.xml b/packages/SystemUI/res/drawable/shortcut_search_background.xml
index 66fc191..d6847f0 100644
--- a/packages/SystemUI/res/drawable/shortcut_search_background.xml
+++ b/packages/SystemUI/res/drawable/shortcut_search_background.xml
@@ -19,8 +19,8 @@
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
     <item>
         <shape android:shape="rectangle">
-            <solid android:color="?androidprv:attr/colorSurface" />
-            <corners android:radius="24dp" />
+            <solid android:color="?androidprv:attr/materialColorSurfaceBright" />
+            <corners android:radius="@dimen/ksh_search_box_corner_radius" />
         </shape>
     </item>
-</layer-list>
\ No newline at end of file
+</layer-list>
diff --git a/packages/SystemUI/res/drawable/shortcut_search_cancel_button.xml b/packages/SystemUI/res/drawable/shortcut_search_cancel_button.xml
index 6c4d4fb..2675906 100644
--- a/packages/SystemUI/res/drawable/shortcut_search_cancel_button.xml
+++ b/packages/SystemUI/res/drawable/shortcut_search_cancel_button.xml
@@ -20,7 +20,7 @@
     <shape android:shape="oval">
       <size android:width="24dp"
         android:height="24dp" />
-      <solid android:color="?androidprv:attr/colorSurface"/>
+      <solid android:color="?androidprv:attr/materialColorSurfaceBright"/>
     </shape>
   </item>
 </ripple>
diff --git a/packages/SystemUI/res/layout/keyboard_shortcut_app_item.xml b/packages/SystemUI/res/layout/keyboard_shortcut_app_item.xml
index a005100..5ab2327 100644
--- a/packages/SystemUI/res/layout/keyboard_shortcut_app_item.xml
+++ b/packages/SystemUI/res/layout/keyboard_shortcut_app_item.xml
@@ -15,13 +15,14 @@
   ~ limitations under the License
   -->
 <com.android.systemui.statusbar.KeyboardShortcutAppItemLayout
+        xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
         xmlns:android="http://schemas.android.com/apk/res/android"
         android:orientation="horizontal"
         android:background="@drawable/list_item_background"
         android:focusable="true"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:minHeight="48dp"
+        android:minHeight="@dimen/ksh_app_item_minimum_height"
         android:paddingBottom="8dp">
     <ImageView
             android:id="@+id/keyboard_shortcuts_icon"
@@ -39,7 +40,8 @@
             android:layout_height="wrap_content"
             android:paddingEnd="12dp"
             android:paddingBottom="4dp"
-            android:textColor="?android:attr/textColorPrimary"
+            android:textColor="?androidprv:attr/materialColorOnSurface"
+            android:textAppearance="?android:attr/textAppearanceMedium"
             android:textSize="16sp"
             android:maxLines="5"
             android:singleLine="false"
diff --git a/packages/SystemUI/res/layout/keyboard_shortcuts_category_short_separator.xml b/packages/SystemUI/res/layout/keyboard_shortcuts_category_short_separator.xml
index 530e46e..76e5b12 100644
--- a/packages/SystemUI/res/layout/keyboard_shortcuts_category_short_separator.xml
+++ b/packages/SystemUI/res/layout/keyboard_shortcuts_category_short_separator.xml
@@ -15,6 +15,6 @@
   limitations under the License
   -->
 <View xmlns:android="http://schemas.android.com/apk/res/android"
-      android:layout_marginTop="8dp"
-      android:layout_marginBottom="0dp"
+      android:layout_marginTop="@dimen/ksh_category_separator_margin"
+      android:layout_marginBottom="@dimen/ksh_category_separator_margin"
       style="@style/ShortcutHorizontalDivider" />
diff --git a/packages/SystemUI/res/layout/keyboard_shortcuts_category_title.xml b/packages/SystemUI/res/layout/keyboard_shortcuts_category_title.xml
index 4f100f6..6e7fde6 100644
--- a/packages/SystemUI/res/layout/keyboard_shortcuts_category_title.xml
+++ b/packages/SystemUI/res/layout/keyboard_shortcuts_category_title.xml
@@ -16,10 +16,12 @@
   ~ limitations under the License
   -->
 <TextView xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
           android:layout_width="match_parent"
           android:layout_height="match_parent"
+          android:textAppearance="?android:attr/textAppearanceMedium"
           android:textSize="14sp"
-          android:fontFamily="sans-serif-medium"
+          android:textColor="?androidprv:attr/materialColorPrimary"
           android:importantForAccessibility="yes"
           android:paddingTop="20dp"
           android:paddingBottom="10dp"/>
diff --git a/packages/SystemUI/res/layout/keyboard_shortcuts_search_view.xml b/packages/SystemUI/res/layout/keyboard_shortcuts_search_view.xml
index f6042e4..2cfd644 100644
--- a/packages/SystemUI/res/layout/keyboard_shortcuts_search_view.xml
+++ b/packages/SystemUI/res/layout/keyboard_shortcuts_search_view.xml
@@ -16,15 +16,21 @@
 
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:background="@drawable/shortcut_dialog_bg"
     android:layout_width="@dimen/ksh_layout_width"
     android:layout_height="wrap_content"
     android:orientation="vertical">
+
+    <com.google.android.material.bottomsheet.BottomSheetDragHandleView
+        android:id="@+id/drag_handle"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"/>
+
     <TextView
         android:id="@+id/shortcut_title"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:layout_marginTop="40dp"
         android:layout_gravity="center_horizontal"
         android:textAppearance="?android:attr/textAppearanceLarge"
         android:textColor="?android:attr/textColorPrimary"
@@ -39,44 +45,47 @@
             android:layout_height="wrap_content"
             android:layout_marginTop="24dp"
             android:layout_marginBottom="24dp"
-            android:layout_marginStart="49dp"
-            android:layout_marginEnd="49dp"
+            android:layout_marginStart="@dimen/ksh_container_horizontal_margin"
+            android:layout_marginEnd="@dimen/ksh_container_horizontal_margin"
             android:padding="16dp"
             android:background="@drawable/shortcut_search_background"
             android:drawableStart="@drawable/ic_shortcutlist_search"
             android:drawablePadding="15dp"
             android:singleLine="true"
-            android:textColor="?android:attr/textColorPrimary"
+            android:textColor="?androidprv:attr/materialColorOnSurfaceVariant"
             android:inputType="text"
             android:textDirection="locale"
             android:textAlignment="viewStart"
             android:hint="@string/keyboard_shortcut_search_list_hint"
-            android:textColorHint="?android:attr/textColorTertiary" />
+            android:textAppearance="@android:style/TextAppearance.Material"
+            android:textSize="16sp"
+            android:textColorHint="?androidprv:attr/materialColorOutline" />
 
         <ImageButton
             android:id="@+id/keyboard_shortcuts_search_cancel"
             android:layout_gravity="center_vertical|end"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_marginEnd="49dp"
+            android:layout_marginEnd="@dimen/ksh_container_horizontal_margin"
             android:padding="16dp"
             android:contentDescription="@string/keyboard_shortcut_clear_text"
             android:src="@drawable/ic_shortcutlist_search_button_cancel"
             android:background="@drawable/shortcut_search_cancel_button"
             style="@android:style/Widget.Material.Button.Borderless.Small"
-            android:pointerIcon="arrow" />
+            android:pointerIcon="arrow"
+            android:visibility="gone" />
     </FrameLayout>
 
     <HorizontalScrollView
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_marginStart="49dp"
+        android:layout_marginStart="@dimen/ksh_container_horizontal_margin"
         android:layout_marginEnd="0dp"
         android:scrollbars="none">
         <LinearLayout
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:gravity="center_vertical"
+            android:layout_gravity="center_vertical"
             android:orientation="horizontal">
             <Button
                 android:id="@+id/shortcut_system"
@@ -113,29 +122,29 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_marginTop="50dp"
-        android:layout_marginStart="49dp"
-        android:layout_marginEnd="49dp"
+        android:layout_marginStart="@dimen/ksh_container_horizontal_margin"
+        android:layout_marginEnd="@dimen/ksh_container_horizontal_margin"
         android:layout_gravity="center_horizontal"
         android:textAppearance="?android:attr/textAppearanceMedium"
         android:textColor="?android:attr/textColorPrimary"
         android:text="@string/keyboard_shortcut_search_list_no_result"/>
 
-    <ScrollView
+    <androidx.core.widget.NestedScrollView
         android:id="@+id/keyboard_shortcuts_scroll_view"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_marginTop="16dp"
-        android:layout_marginStart="49dp"
-        android:layout_marginEnd="49dp"
+        android:layout_marginStart="@dimen/ksh_container_horizontal_margin"
+        android:layout_marginEnd="@dimen/ksh_container_horizontal_margin"
         android:overScrollMode="never"
-        android:layout_marginBottom="16dp"
+        android:clipToPadding="false"
         android:scrollbars="none">
         <LinearLayout
             android:id="@+id/keyboard_shortcuts_container"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:orientation="vertical"/>
-    </ScrollView>
+    </androidx.core.widget.NestedScrollView>
     <!-- Required for stretching to full available height when the items in the scroll view
          occupy less space then the full height -->
     <View
diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml
index 55606aa..56ebc06 100644
--- a/packages/SystemUI/res/values-land/dimens.xml
+++ b/packages/SystemUI/res/values-land/dimens.xml
@@ -94,4 +94,7 @@
 
     <dimen name="keyguard_indication_margin_bottom">8dp</dimen>
     <dimen name="lock_icon_margin_bottom">24dp</dimen>
+
+    <!-- Keyboard shortcuts helper -->
+    <dimen name="ksh_container_horizontal_margin">48dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index f2288a4..b95ee56 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -978,6 +978,7 @@
     <dimen name="assist_disclosure_shadow_thickness">1.5dp</dimen>
 
     <!-- Keyboard shortcuts helper -->
+    <dimen name="ksh_container_horizontal_margin">32dp</dimen>
     <dimen name="ksh_layout_width">@dimen/match_parent</dimen>
     <dimen name="ksh_item_text_size">14sp</dimen>
     <dimen name="ksh_item_padding">0dp</dimen>
@@ -985,6 +986,11 @@
     <dimen name="ksh_icon_scaled_size">18dp</dimen>
     <dimen name="ksh_key_view_padding_vertical">4dp</dimen>
     <dimen name="ksh_key_view_padding_horizontal">12dp</dimen>
+    <dimen name="ksh_button_corner_radius">12dp</dimen>
+    <dimen name="ksh_dialog_top_corner_radius">28dp</dimen>
+    <dimen name="ksh_search_box_corner_radius">100dp</dimen>
+    <dimen name="ksh_app_item_minimum_height">64dp</dimen>
+    <dimen name="ksh_category_separator_margin">16dp</dimen>
 
     <!-- The size of corner radius of the arrow in the onboarding toast. -->
     <dimen name="recents_onboarding_toast_arrow_corner_radius">2dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 71353b6..2446039 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2023,12 +2023,12 @@
     <!-- User visible title for the keyboard shortcut that pulls up Notes app for quick memo. [CHAR LIMIT=70] -->
     <string name="group_system_quick_memo">Take a note</string>
 
-    <!-- User visible title for the system multitasking keyboard shortcuts list. [CHAR LIMIT=70] -->
-    <string name="keyboard_shortcut_group_system_multitasking">System multitasking</string>
-    <!-- User visible title for the keyboard shortcut that enters split screen with current app to RHS [CHAR LIMIT=70] -->
-    <string name="system_multitasking_rhs">Enter split screen with current app to RHS</string>
-    <!-- User visible title for the keyboard shortcut that enters split screen with current app to LHS [CHAR LIMIT=70] -->
-    <string name="system_multitasking_lhs">Enter split screen with current app to LHS</string>
+    <!-- User visible title for the multitasking keyboard shortcuts list. [CHAR LIMIT=70] -->
+    <string name="keyboard_shortcut_group_system_multitasking">Multitasking</string>
+    <!-- User visible title for the keyboard shortcut that enters split screen with current app on the right [CHAR LIMIT=70] -->
+    <string name="system_multitasking_rhs">Use split screen with current app on the right</string>
+    <!-- User visible title for the keyboard shortcut that enters split screen with current app on the left [CHAR LIMIT=70] -->
+    <string name="system_multitasking_lhs">Use split screen with current app on the left</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] -->
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 6462d02..9da4f79 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -224,6 +224,7 @@
     <style name="TextAppearance.AuthCredential.ContentViewListItem" parent="TextAppearance.Material3.BodySmall">
         <item name="android:textColor">?androidprv:attr/materialColorOnSurfaceVariant</item>
         <item name="android:paddingTop">@dimen/biometric_prompt_content_list_item_padding_top</item>
+        <item name="android:breakStrategy">high_quality</item>
     </style>
 
     <style name="TextAppearance.AuthCredential.Indicator" parent="TextAppearance.Material3.BodyMedium">
@@ -365,6 +366,21 @@
         <item name="android:layout_height">wrap_content</item>
     </style>
 
+    <style name="KeyboardShortcutHelper" parent="@android:style/Theme.DeviceDefault.Settings">
+        <!-- Needed to be able to use BottomSheetDragHandleView -->
+        <item name="android:windowActionBar">false</item>
+        <item name="bottomSheetDragHandleStyle">@style/KeyboardShortcutHelper.BottomSheet.DragHandle</item>
+    </style>
+
+    <style name="KeyboardShortcutHelper.BottomSheet.DragHandle" parent="Widget.Material3.BottomSheet.DragHandle">
+        <item name="tint">?androidprv:attr/materialColorOutlineVariant</item>
+    </style>
+
+    <style name="KeyboardShortcutHelper.BottomSheetDialogAnimation">
+        <item name="android:windowEnterAnimation">@anim/slide_in_up</item>
+        <item name="android:windowExitAnimation">@anim/slide_out_down</item>
+    </style>
+
     <style name="BrightnessDialogContainer" parent="@style/BaseBrightnessDialogContainer" />
 
     <style name="Animation" />
@@ -1597,14 +1613,15 @@
         <item name="android:layout_marginEnd">12dp</item>
         <item name="android:paddingLeft">24dp</item>
         <item name="android:paddingRight">24dp</item>
-        <item name="android:minHeight">40dp</item>
+        <item name="android:minHeight">36dp</item>
+        <item name="android:minWidth">120dp</item>
         <item name="android:stateListAnimator">@*android:anim/flat_button_state_list_anim_material</item>
         <item name="android:pointerIcon">arrow</item>
     </style>
 
     <style name="ShortcutHorizontalDivider">
-        <item name="android:layout_width">120dp</item>
-        <item name="android:layout_height">1dp</item>
+        <item name="android:layout_width">132dp</item>
+        <item name="android:layout_height">2dp</item>
         <item name="android:layout_gravity">center_horizontal</item>
         <item name="android:background">?android:attr/dividerHorizontal</item>
     </style>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
index 4632914..dcc1440 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
@@ -155,5 +155,10 @@
      */
     oneway void animateNavBarLongPress(boolean isTouchDown, boolean shrink, long durationMs) = 54;
 
-    // Next id = 55
+    /**
+     * Set the override value for home button long press duration in ms and slop multiplier.
+     */
+    oneway void setOverrideHomeButtonLongPress(long duration, float slopMultiplier) = 55;
+
+    // Next id = 56
 }
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
index 8ca083f..5df7fc9 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
@@ -243,12 +243,6 @@
         }
 
         // Authentication failed.
-
-        if (tryAutoConfirm) {
-            // Auto-confirm is active, the failed attempt should have no side-effects.
-            return AuthenticationResult.FAILED
-        }
-
         repository.reportAuthenticationAttempt(isSuccessful = false)
 
         if (authenticationResult.lockoutDurationMs > 0) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 9d79e87..b25c3da 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -403,6 +403,8 @@
             if (DeviceEntryUdfpsRefactor.isEnabled()) {
                 if (mOverlay != null && mOverlay.getRequestReason() == REASON_AUTH_KEYGUARD) {
                     mOverlay.updateOverlayParams(mOverlayParams);
+                } else {
+                    redrawOverlay();
                 }
             } else {
                 final boolean wasShowingAlternateBouncer =
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt
index 88aef56..7ccac03 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt
@@ -31,18 +31,19 @@
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
 import android.view.ViewTreeObserver
 import android.widget.Button
 import android.widget.LinearLayout
 import android.widget.Space
 import android.widget.TextView
-import androidx.lifecycle.lifecycleScope
 import com.android.settingslib.Utils
 import com.android.systemui.biometrics.ui.BiometricPromptLayout
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.res.R
 import kotlin.math.ceil
-import kotlinx.coroutines.launch
+
+private const val TAG = "BiometricCustomizedViewBinder"
 
 /** Sub-binder for [BiometricPromptLayout.customized_view_container]. */
 object BiometricCustomizedViewBinder {
@@ -52,21 +53,20 @@
         legacyCallback: Spaghetti.Callback
     ) {
         customizedViewContainer.repeatWhenAttached { containerView ->
-            lifecycleScope.launch {
-                if (contentView == null) {
-                    containerView.visibility = View.GONE
-                    return@launch
-                }
+            if (contentView == null) {
+                containerView.visibility = View.GONE
+                return@repeatWhenAttached
+            }
 
-                containerView.width { containerWidth ->
-                    if (containerWidth == 0) {
-                        return@width
-                    }
-                    (containerView as LinearLayout).addView(
-                        contentView.toView(containerView.context, containerWidth, legacyCallback)
-                    )
-                    containerView.visibility = View.VISIBLE
+            containerView.width { containerWidth ->
+                if (containerWidth == 0) {
+                    return@width
                 }
+                (containerView as LinearLayout).addView(
+                    contentView.toView(containerView.context, containerWidth, legacyCallback),
+                    LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
+                )
+                containerView.visibility = View.VISIBLE
             }
         }
     }
@@ -118,50 +118,41 @@
     containerViewWidth: Int
 ): View {
     val inflater = LayoutInflater.from(context)
-    val resources = context.resources
+    context.resources
     val contentView =
         inflater.inflateContentView(
             R.layout.biometric_prompt_vertical_list_content_layout,
             description
         )
+    val listItemsToShow = ArrayList<PromptContentItem>(listItems)
     // Show two column by default, once there is an item exceeding max lines, show single
     // item instead.
     val showTwoColumn =
-        listItems.all { !it.doesExceedMaxLinesIfTwoColumn(context, containerViewWidth) }
-    var currRowView = createNewRowLayout(inflater)
-    for (item in listItems) {
-        val itemView = item.toView(context, inflater)
-        // If this item will be in the first row (contentView only has description view) and
-        // description is empty, remove top padding of this item.
-        if (contentView.childCount == 1 && description.isNullOrEmpty()) {
-            itemView.setPadding(
-                itemView.paddingLeft,
-                0,
-                itemView.paddingRight,
-                itemView.paddingBottom
-            )
-        }
-        currRowView.addView(itemView)
-
-        // If this is the first item in the current row, add space behind it.
-        if (currRowView.childCount == 1 && showTwoColumn) {
-            currRowView.addSpaceView(
-                resources.getDimensionPixelSize(
-                    R.dimen.biometric_prompt_content_space_width_between_items
-                ),
-                MATCH_PARENT
-            )
-        }
-
-        // If there are already two items (plus the space view) in the current row, or it
-        // should be one column, start a new row
-        if (currRowView.childCount == 3 || !showTwoColumn) {
-            contentView.addView(currRowView)
-            currRowView = createNewRowLayout(inflater)
-        }
+        listItemsToShow.all { !it.doesExceedMaxLinesIfTwoColumn(context, containerViewWidth) }
+    // If should show two columns and there are more than one items, make listItems always have odd
+    // number items.
+    if (showTwoColumn && listItemsToShow.size > 1 && listItemsToShow.size % 2 == 1) {
+        listItemsToShow.add(dummyItem())
     }
-    if (currRowView.childCount > 0) {
-        contentView.addView(currRowView)
+    var currRow = createNewRowLayout(inflater)
+    for (i in 0 until listItemsToShow.size) {
+        val item = listItemsToShow[i]
+        val itemView = item.toView(context, inflater)
+        contentView.removeTopPaddingForFirstRow(description, itemView)
+
+        // If there should be two column, and there is already one item in the current row, add
+        // space between two items.
+        if (showTwoColumn && currRow.childCount == 1) {
+            currRow.addSpaceViewBetweenListItem()
+        }
+        currRow.addView(itemView)
+
+        // If there should be one column, or there are already two items (plus the space view) in
+        // the current row, or it's already the last item, start a new row
+        if (!showTwoColumn || currRow.childCount == 3 || i == listItemsToShow.size - 1) {
+            contentView.addView(currRow)
+            currRow = createNewRowLayout(inflater)
+        }
     }
     return contentView
 }
@@ -170,10 +161,6 @@
     return inflater.inflate(R.layout.biometric_prompt_content_row_layout, null) as LinearLayout
 }
 
-private fun LinearLayout.addSpaceView(width: Int, height: Int) {
-    addView(Space(context), LinearLayout.LayoutParams(width, height))
-}
-
 private fun PromptContentItem.doesExceedMaxLinesIfTwoColumn(
     context: Context,
     containerViewWidth: Int,
@@ -194,7 +181,10 @@
             val contentViewPadding =
                 resources.getDimensionPixelSize(R.dimen.biometric_prompt_content_padding_horizontal)
             val listItemPadding = getListItemPadding(resources)
-            val maxWidth = containerViewWidth / 2 - contentViewPadding - listItemPadding
+            var maxWidth = containerViewWidth / 2 - contentViewPadding - listItemPadding
+            // Reduce maxWidth a bit since paint#measureText is not accurate. See b/330909104 for
+            // more context.
+            maxWidth -= contentViewPadding / 2
 
             val paint = TextPaint()
             val attributes =
@@ -224,6 +214,7 @@
     inflater: LayoutInflater,
 ): TextView {
     val resources = context.resources
+    // Somehow xml layout params settings doesn't work, set it again here.
     val textView =
         inflater.inflate(R.layout.biometric_prompt_content_row_item_text_view, null) as TextView
     val lp = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1f)
@@ -251,6 +242,29 @@
     return textView
 }
 
+/* [contentView] function */
+private fun LinearLayout.addSpaceViewBetweenListItem() =
+    addView(
+        Space(context),
+        LinearLayout.LayoutParams(
+            resources.getDimensionPixelSize(
+                R.dimen.biometric_prompt_content_space_width_between_items
+            ),
+            MATCH_PARENT
+        )
+    )
+
+/* [contentView] function*/
+private fun LinearLayout.removeTopPaddingForFirstRow(description: String?, itemView: TextView) {
+    // If this item will be in the first row (contentView only has description view and
+    // description is empty), remove top padding of this item.
+    if (description.isNullOrEmpty() && childCount == 1) {
+        itemView.setPadding(itemView.paddingLeft, 0, itemView.paddingRight, itemView.paddingBottom)
+    }
+}
+
+private fun dummyItem(): PromptContentItem = PromptContentItemPlainText("")
+
 private fun PromptContentItem.getListItemPadding(resources: Resources): Int {
     var listItemPadding =
         resources.getDimensionPixelSize(
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt
index f1a0e5e..78811a9 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt
@@ -12,14 +12,15 @@
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
 import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.flags.Flags.COMPOSE_BOUNCER_ENABLED
 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
 import com.android.systemui.log.BouncerLogger
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import dagger.Lazy
 import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 
 /** Helper data class that allows to lazy load all the dependencies of the legacy bouncer. */
+@OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 data class LegacyBouncerDependencies
 @Inject
@@ -59,7 +60,7 @@
     private val composeBouncerDependencies: Lazy<ComposeBouncerDependencies>,
 ) {
     fun bind(view: ViewGroup) {
-        if (COMPOSE_BOUNCER_ENABLED && composeBouncerFlags.isOnlyComposeBouncerEnabled()) {
+        if (composeBouncerFlags.isOnlyComposeBouncerEnabled()) {
             val deps = composeBouncerDependencies.get()
             ComposeBouncerViewBinder.bind(
                 view,
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
index 62da5c0..12cac92 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
@@ -99,7 +99,7 @@
             .map { if (it) ActionButtonAppearance.Hidden else ActionButtonAppearance.Shown }
             .stateIn(
                 scope = viewModelScope,
-                started = SharingStarted.Eagerly,
+                started = SharingStarted.WhileSubscribed(),
                 initialValue = ActionButtonAppearance.Hidden,
             )
 
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/ViewExt.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/ViewExt.kt
index ce798ba..7dd883b 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/view/ViewExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/ViewExt.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.common.ui.view
 
 import android.view.View
+import kotlinx.coroutines.DisposableHandle
 
 /**
  * Set this view's [View#importantForAccessibility] to [View#IMPORTANT_FOR_ACCESSIBILITY_YES] or
@@ -43,3 +44,10 @@
     }
     return null
 }
+
+/** Adds a [View.OnLayoutChangeListener] and provides a [DisposableHandle] for teardown. */
+fun View.onLayoutChanged(onLayoutChanged: (v: View) -> Unit): DisposableHandle {
+    val listener = View.OnLayoutChangeListener { v, _, _, _, _, _, _, _, _ -> onLayoutChanged(v) }
+    addOnLayoutChangeListener(listener)
+    return DisposableHandle { removeOnLayoutChangeListener(listener) }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 73878b6..640534c 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -410,13 +410,6 @@
     val CLIPBOARD_SHARED_TRANSITIONS =
             unreleasedFlag("clipboard_shared_transitions", teamfood = true)
 
-    /**
-     * Whether the compose bouncer is enabled. This ensures ProGuard can
-     * remove unused code from our APK at compile time.
-     */
-    // TODO(b/280877228): Tracking Bug
-    @JvmField val COMPOSE_BOUNCER_ENABLED = false
-
     // 1900
     @JvmField val NOTE_TASKS = releasedFlag("keycode_flag")
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index 9f7e0d4..e6e6ff6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -208,7 +208,7 @@
                 chipbarCoordinator,
                 screenOffAnimationController,
                 shadeInteractor,
-                { keyguardStatusViewController!!.getClockController() },
+                clockInteractor,
                 interactionJankMonitor,
                 deviceEntryHapticsInteractor,
                 vibratorHelper,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
index 99b691e..d551c9b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
@@ -17,7 +17,9 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import android.util.Log
 import com.android.keyguard.ClockEventController
+import com.android.keyguard.KeyguardClockSwitch
 import com.android.keyguard.KeyguardClockSwitch.ClockSize
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.data.repository.KeyguardClockRepository
@@ -54,6 +56,15 @@
         keyguardClockRepository.setClockSize(size)
     }
 
+    val renderedClockId: ClockId
+        get() {
+            return clock?.let { clock -> clock.config.id }
+                ?: run {
+                    Log.e(TAG, "No clock is available")
+                    KeyguardClockSwitch.MISSING_CLOCK_ID
+                }
+        }
+
     fun animateFoldToAod(foldFraction: Float) {
         clock?.let { clock ->
             clock.smallClock.animations.fold(foldFraction)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
index 68ea5d0..141cca3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.log.core.LogLevel.VERBOSE
 import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
 import javax.inject.Inject
@@ -69,9 +70,11 @@
             }
         }
 
-        scope.launch {
-            sharedNotificationContainerViewModel.bounds.collect {
-                logger.log(TAG, VERBOSE, "Notif: bounds", it)
+        if (!SceneContainerFlag.isEnabled) {
+            scope.launch {
+                sharedNotificationContainerViewModel.bounds.collect {
+                    logger.log(TAG, VERBOSE, "Notif: bounds", it)
+                }
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
index 1382468..486320a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
@@ -34,6 +34,7 @@
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.app.tracing.coroutines.launch
+import com.android.internal.policy.SystemBarUtils
 import com.android.systemui.customization.R as customizationR
 import com.android.systemui.keyguard.shared.model.SettingsClockSize
 import com.android.systemui.keyguard.ui.preview.KeyguardPreviewRenderer
@@ -116,7 +117,7 @@
             constrainWidth(R.id.lockscreen_clock_view_large, ConstraintSet.WRAP_CONTENT)
             constrainHeight(R.id.lockscreen_clock_view_large, ConstraintSet.MATCH_CONSTRAINT)
             val largeClockTopMargin =
-                context.resources.getDimensionPixelSize(R.dimen.status_bar_height) +
+                SystemBarUtils.getStatusBarHeight(context) +
                     context.resources.getDimensionPixelSize(
                         customizationR.dimen.small_clock_padding_top
                     ) +
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index 5f50f7e..0249abd 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -36,7 +36,6 @@
 import com.android.app.tracing.coroutines.launch
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD
-import com.android.keyguard.KeyguardClockSwitch.MISSING_CLOCK_ID
 import com.android.systemui.Flags.newAodTransition
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
@@ -47,6 +46,7 @@
 import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
 import com.android.systemui.keyguard.KeyguardViewMediator
 import com.android.systemui.keyguard.MigrateClocksToBlueprint
+import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
@@ -55,7 +55,6 @@
 import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.FalsingManager
-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.statusbar.CrossFadeHelper
@@ -69,7 +68,6 @@
 import com.android.systemui.util.ui.isAnimating
 import com.android.systemui.util.ui.stopAnimating
 import com.android.systemui.util.ui.value
-import javax.inject.Provider
 import kotlin.math.min
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.DisposableHandle
@@ -93,7 +91,7 @@
         chipbarCoordinator: ChipbarCoordinator,
         screenOffAnimationController: ScreenOffAnimationController,
         shadeInteractor: ShadeInteractor,
-        clockControllerProvider: Provider<ClockController>?,
+        clockInteractor: KeyguardClockInteractor,
         interactionJankMonitor: InteractionJankMonitor?,
         deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor?,
         vibratorHelper: VibratorHelper?,
@@ -281,14 +279,11 @@
                                 viewModel.goneToAodTransition.collect {
                                     when (it.transitionState) {
                                         TransitionState.STARTED -> {
-                                            val clockId =
-                                                clockControllerProvider?.get()?.config?.id
-                                                    ?: MISSING_CLOCK_ID
+                                            val clockId = clockInteractor.renderedClockId
                                             val builder =
                                                 InteractionJankMonitor.Configuration.Builder
                                                     .withView(CUJ_SCREEN_OFF_SHOW_AOD, view)
                                                     .setTag(clockId)
-
                                             jankMonitor.begin(builder)
                                         }
                                         TransitionState.CANCELED ->
@@ -345,12 +340,6 @@
                 }
             }
 
-        if (!MigrateClocksToBlueprint.isEnabled) {
-            burnInParams.update { current ->
-                current.copy(clockControllerProvider = clockControllerProvider)
-            }
-        }
-
         if (MigrateClocksToBlueprint.isEnabled) {
             burnInParams.update { current ->
                 current.copy(translationY = { childViews[burnInLayerId]?.translationY })
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 9195b4f..42bd4af 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
@@ -47,6 +47,7 @@
 import androidx.constraintlayout.widget.ConstraintSet.START
 import androidx.constraintlayout.widget.ConstraintSet.TOP
 import androidx.core.view.isInvisible
+import com.android.internal.policy.SystemBarUtils
 import com.android.keyguard.ClockEventController
 import com.android.keyguard.KeyguardClockSwitch
 import com.android.systemui.animation.view.LaunchableImageView
@@ -60,6 +61,7 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
 import com.android.systemui.keyguard.MigrateClocksToBlueprint
+import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.ui.binder.KeyguardPreviewClockViewBinder
 import com.android.systemui.keyguard.ui.binder.KeyguardPreviewSmartspaceViewBinder
 import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
@@ -140,6 +142,7 @@
     private val secureSettings: SecureSettings,
     private val communalTutorialViewModel: CommunalTutorialIndicatorViewModel,
     private val defaultShortcutsSection: DefaultShortcutsSection,
+    private val keyguardClockInteractor: KeyguardClockInteractor,
 ) {
     val hostToken: IBinder? = bundle.getBinder(KEY_HOST_TOKEN)
     private val width: Int = bundle.getInt(KEY_VIEW_WIDTH)
@@ -364,6 +367,7 @@
             ),
         )
     }
+
     @OptIn(ExperimentalCoroutinesApi::class)
     private fun setupKeyguardRootView(previewContext: Context, rootView: FrameLayout) {
         val keyguardRootView = KeyguardRootView(previewContext, null)
@@ -377,7 +381,7 @@
                     chipbarCoordinator,
                     screenOffAnimationController,
                     shadeInteractor,
-                    null, // clock provider only needed for burn in
+                    keyguardClockInteractor,
                     null, // jank monitor not required for preview mode
                     null, // device entry haptics not required preview mode
                     null, // device entry haptics not required for preview mode
@@ -536,7 +540,7 @@
                     )
                 )
             layoutParams.topMargin =
-                KeyguardPreviewSmartspaceViewModel.getStatusBarHeight(resources) +
+                SystemBarUtils.getStatusBarHeight(previewContext) +
                     resources.getDimensionPixelSize(
                         com.android.systemui.customization.R.dimen.small_clock_padding_top
                     )
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 2054932..4ddd5711 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
@@ -33,10 +33,8 @@
 import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
 import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
 import com.android.systemui.keyguard.ui.StateToValue
-import com.android.systemui.plugins.clocks.ClockController
 import com.android.systemui.res.R
 import javax.inject.Inject
-import javax.inject.Provider
 import kotlin.math.max
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
@@ -128,12 +126,12 @@
                 yDimenResourceId = R.dimen.burn_in_prevention_offset_y
             ),
         ) { interpolated, burnIn ->
+            val useAltAod =
+                keyguardClockViewModel.currentClock.value?.let { clock ->
+                    clock.config.useAlternateSmartspaceAODTransition
+                } == true
             val useScaleOnly =
-                (clockController(params.clockControllerProvider)
-                    ?.get()
-                    ?.config
-                    ?.useAlternateSmartspaceAODTransition
-                    ?: false) && keyguardClockViewModel.clockSize.value == KeyguardClockSwitch.LARGE
+                useAltAod && keyguardClockViewModel.clockSize.value == KeyguardClockSwitch.LARGE
 
             if (useScaleOnly) {
                 BurnInModel(
@@ -164,21 +162,10 @@
             }
         }
     }
-
-    private fun clockController(
-        provider: Provider<ClockController>?,
-    ): Provider<ClockController>? {
-        return if (MigrateClocksToBlueprint.isEnabled) {
-            Provider { keyguardClockViewModel.currentClock.value }
-        } else {
-            provider
-        }
-    }
 }
 
 /** UI-sourced parameters to pass into the various methods of [AodBurnInViewModel]. */
 data class BurnInParameters(
-    val clockControllerProvider: Provider<ClockController>? = null,
     /** System insets that keyguard needs to stay out of */
     val topInset: Int = 0,
     /** The min y-value of the visible elements on lockscreen */
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 f6f3bb1..c9251c7 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
@@ -19,6 +19,7 @@
 import android.content.Context
 import androidx.annotation.VisibleForTesting
 import androidx.constraintlayout.helper.widget.Layer
+import com.android.internal.policy.SystemBarUtils
 import com.android.keyguard.KeyguardClockSwitch.LARGE
 import com.android.keyguard.KeyguardClockSwitch.SMALL
 import com.android.systemui.customization.R as customizationR
@@ -165,7 +166,7 @@
 
     companion object {
         fun getLargeClockTopMargin(context: Context): Int {
-            return context.resources.getDimensionPixelSize(R.dimen.status_bar_height) +
+            return SystemBarUtils.getStatusBarHeight(context) +
                 context.resources.getDimensionPixelSize(
                     customizationR.dimen.small_clock_padding_top
                 ) +
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt
index a90634e..b57e3ec 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import android.content.Context
-import android.content.res.Resources
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.shared.model.SettingsClockSize
@@ -89,14 +88,4 @@
             }
         }
     }
-    companion object {
-        fun getStatusBarHeight(resource: Resources): Int {
-            var result = 0
-            val resourceId: Int = resource.getIdentifier("status_bar_height", "dimen", "android")
-            if (resourceId > 0) {
-                result = resource.getDimensionPixelSize(resourceId)
-            }
-            return result
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
index 1c11178..5dafd94 100644
--- a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
+++ b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
@@ -26,7 +26,9 @@
 import androidx.lifecycle.lifecycleScope
 import com.android.app.tracing.coroutines.createCoroutineTracingContext
 import com.android.app.tracing.coroutines.launch
+import com.android.systemui.Flags.coroutineTracing
 import com.android.systemui.util.Assert
+import com.android.systemui.util.Compile
 import kotlin.coroutines.CoroutineContext
 import kotlin.coroutines.EmptyCoroutineContext
 import kotlinx.coroutines.Dispatchers
@@ -69,6 +71,12 @@
     // presumably want to call view methods that require being called from said UI thread.
     val lifecycleCoroutineContext =
         Dispatchers.Main + createCoroutineTracingContext() + coroutineContext
+    val traceName =
+        if (Compile.IS_DEBUG && coroutineTracing()) {
+            traceSectionName()
+        } else {
+            DEFAULT_TRACE_NAME
+        }
     var lifecycleOwner: ViewLifecycleOwner? = null
     val onAttachListener =
         object : View.OnAttachStateChangeListener {
@@ -77,6 +85,7 @@
                 lifecycleOwner?.onDestroy()
                 lifecycleOwner =
                     createLifecycleOwnerAndRun(
+                        traceName,
                         view,
                         lifecycleCoroutineContext,
                         block,
@@ -93,6 +102,7 @@
     if (view.isAttachedToWindow) {
         lifecycleOwner =
             createLifecycleOwnerAndRun(
+                traceName,
                 view,
                 lifecycleCoroutineContext,
                 block,
@@ -109,18 +119,14 @@
 }
 
 private fun createLifecycleOwnerAndRun(
+    nameForTrace: String,
     view: View,
     coroutineContext: CoroutineContext,
     block: suspend LifecycleOwner.(View) -> Unit,
 ): ViewLifecycleOwner {
     return ViewLifecycleOwner(view).apply {
         onCreate()
-        lifecycleScope.launch(
-            "ViewLifecycleOwner(${view::class.java.simpleName})",
-            coroutineContext
-        ) {
-            block(view)
-        }
+        lifecycleScope.launch(nameForTrace, coroutineContext) { block(view) }
     }
 }
 
@@ -186,3 +192,24 @@
             }
     }
 }
+
+private fun isFrameInteresting(frame: StackWalker.StackFrame): Boolean =
+    frame.className != CURRENT_CLASS_NAME && frame.className != JAVA_ADAPTER_CLASS_NAME
+
+/** Get a name for the trace section include the name of the call site. */
+private fun traceSectionName(): String {
+    val interestingFrame =
+        StackWalker.getInstance().walk { stream ->
+            stream.filter(::isFrameInteresting).limit(5).findFirst()
+        }
+    if (interestingFrame.isPresent) {
+        val frame = interestingFrame.get()
+        return "${frame.className}#${frame.methodName}:${frame.lineNumber} [$DEFAULT_TRACE_NAME]"
+    } else {
+        return DEFAULT_TRACE_NAME
+    }
+}
+
+private const val DEFAULT_TRACE_NAME = "repeatWhenAttached"
+private const val CURRENT_CLASS_NAME = "com.android.systemui.lifecycle.RepeatWhenAttachedKt"
+private const val JAVA_ADAPTER_CLASS_NAME = "com.android.systemui.util.kotlin.JavaAdapterKt"
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
index e861ddf..da9e00d 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
@@ -16,11 +16,8 @@
 
 package com.android.systemui.mediaprojection.permission;
 
-import static android.Manifest.permission.LOG_COMPAT_CHANGE;
-import static android.Manifest.permission.READ_COMPAT_CHANGE_CONFIG;
 import static android.media.projection.IMediaProjectionManager.EXTRA_PACKAGE_REUSING_GRANTED_CONSENT;
 import static android.media.projection.IMediaProjectionManager.EXTRA_USER_REVIEW_GRANTED_CONSENT;
-import static android.media.projection.MediaProjectionManager.OVERRIDE_DISABLE_MEDIA_PROJECTION_SINGLE_APP_OPTION;
 import static android.media.projection.ReviewGrantedConsentResult.RECORD_CANCEL;
 import static android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_DISPLAY;
 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
@@ -29,13 +26,11 @@
 import static com.android.systemui.mediaprojection.permission.ScreenShareOptionKt.SINGLE_APP;
 
 import android.annotation.Nullable;
-import android.annotation.RequiresPermission;
 import android.app.Activity;
 import android.app.ActivityManager;
 import android.app.ActivityOptions.LaunchCookie;
 import android.app.AlertDialog;
 import android.app.StatusBarManager;
-import android.app.compat.CompatChanges;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
@@ -113,7 +108,6 @@
     }
 
     @Override
-    @RequiresPermission(allOf = {READ_COMPAT_CHANGE_CONFIG, LOG_COMPAT_CHANGE})
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
@@ -241,10 +235,6 @@
         // the correct screen width when in split screen.
         Context dialogContext = getApplicationContext();
         if (isPartialScreenSharingEnabled()) {
-            final boolean overrideDisableSingleAppOption =
-                    CompatChanges.isChangeEnabled(
-                            OVERRIDE_DISABLE_MEDIA_PROJECTION_SINGLE_APP_OPTION,
-                            mPackageName, getHostUserHandle());
             MediaProjectionPermissionDialogDelegate delegate =
                     new MediaProjectionPermissionDialogDelegate(
                             dialogContext,
@@ -256,7 +246,6 @@
                             },
                             () -> finish(RECORD_CANCEL, /* projection= */ null),
                             appName,
-                            overrideDisableSingleAppOption,
                             mUid,
                             mMediaProjectionMetricsLogger);
             mDialog =
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt
index 8858041a..0f54e93 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt
@@ -30,12 +30,11 @@
     private val onStartRecordingClicked: Consumer<MediaProjectionPermissionDialogDelegate>,
     private val onCancelClicked: Runnable,
     private val appName: String?,
-    private val forceShowPartialScreenshare: Boolean,
     hostUid: Int,
     mediaProjectionMetricsLogger: MediaProjectionMetricsLogger,
 ) :
     BaseMediaProjectionPermissionDialogDelegate<AlertDialog>(
-        createOptionList(context, appName, mediaProjectionConfig, forceShowPartialScreenshare),
+        createOptionList(context, appName, mediaProjectionConfig),
         appName,
         hostUid,
         mediaProjectionMetricsLogger
@@ -66,8 +65,7 @@
         private fun createOptionList(
             context: Context,
             appName: String?,
-            mediaProjectionConfig: MediaProjectionConfig?,
-            overrideDisableSingleAppOption: Boolean = false,
+            mediaProjectionConfig: MediaProjectionConfig?
         ): List<ScreenShareOption> {
             val singleAppWarningText =
                 if (appName == null) {
@@ -82,13 +80,8 @@
                     R.string.media_projection_entry_app_permission_dialog_warning_entire_screen
                 }
 
-            // The single app option should only be disabled if there is an app name provided,
-            // the client has setup a MediaProjection with
-            // MediaProjectionConfig#createConfigForDefaultDisplay, AND it hasn't been overridden by
-            // the OVERRIDE_DISABLE_SINGLE_APP_OPTION per-app override.
             val singleAppOptionDisabled =
                 appName != null &&
-                    !overrideDisableSingleAppOption &&
                     mediaProjectionConfig?.regionToCapture ==
                         MediaProjectionConfig.CAPTURE_REGION_FIXED_DISPLAY
 
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index 4fe3a11..ade56c4 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -85,6 +85,7 @@
 import android.view.MotionEvent;
 import android.view.Surface;
 import android.view.View;
+import android.view.ViewConfiguration;
 import android.view.ViewTreeObserver;
 import android.view.ViewTreeObserver.InternalInsetsInfo;
 import android.view.ViewTreeObserver.OnComputeInternalInsetsListener;
@@ -186,6 +187,7 @@
     /** Allow some time inbetween the long press for back and recents. */
     private static final int LOCK_TO_APP_GESTURE_TOLERANCE = 200;
     private static final long AUTODIM_TIMEOUT_MS = 2250;
+    private static final float QUICKSTEP_TOUCH_SLOP_RATIO_TWO_BUTTON = 3f;
 
     private final Context mContext;
     private final Bundle mSavedState;
@@ -223,6 +225,7 @@
     private final int mNavColorSampleMargin;
     private EdgeBackGestureHandler mEdgeBackGestureHandler;
     private NavigationBarFrame mFrame;
+    private MotionEvent mCurrentDownEvent;
 
     private @WindowVisibleState int mNavigationBarWindowState = WINDOW_STATE_SHOWING;
 
@@ -238,6 +241,8 @@
     private int mLayoutDirection;
 
     private Optional<Long> mHomeButtonLongPressDurationMs;
+    private Optional<Long> mOverrideHomeButtonLongPressDurationMs = Optional.empty();
+    private Optional<Float> mOverrideHomeButtonLongPressSlopMultiplier = Optional.empty();
 
     /** @see android.view.WindowInsetsController#setSystemBarsAppearance(int, int) */
     private @Appearance int mAppearance;
@@ -405,6 +410,25 @@
         }
 
         @Override
+        public void setOverrideHomeButtonLongPress(long duration, float slopMultiplier) {
+            mOverrideHomeButtonLongPressDurationMs = Optional.of(duration)
+                    .filter(value -> value > 0);
+            mOverrideHomeButtonLongPressSlopMultiplier = Optional.of(slopMultiplier)
+                    .filter(value -> value > 0);
+            if (mOverrideHomeButtonLongPressDurationMs.isPresent()) {
+                Log.d(TAG, "Receive duration override: "
+                        + mOverrideHomeButtonLongPressDurationMs.get());
+            }
+            if (mOverrideHomeButtonLongPressSlopMultiplier.isPresent()) {
+                Log.d(TAG, "Receive slop multiplier override: "
+                        + mOverrideHomeButtonLongPressSlopMultiplier.get());
+            }
+            if (mView != null) {
+                reconfigureHomeLongClick();
+            }
+        }
+
+        @Override
         public void onHomeRotationEnabled(boolean enabled) {
             mView.getRotationButtonController().setHomeRotationEnabled(enabled);
         }
@@ -1016,7 +1040,10 @@
         if (mView.getHomeButton().getCurrentView() == null) {
             return;
         }
-        if (mHomeButtonLongPressDurationMs.isPresent() || !mLongPressHomeEnabled) {
+        if (mHomeButtonLongPressDurationMs.isPresent()
+                || mOverrideHomeButtonLongPressDurationMs.isPresent()
+                || mOverrideHomeButtonLongPressSlopMultiplier.isPresent()
+                || !mLongPressHomeEnabled) {
             mView.getHomeButton().getCurrentView().setLongClickable(false);
             mView.getHomeButton().getCurrentView().setHapticFeedbackEnabled(false);
             mView.getHomeButton().setOnLongClickListener(null);
@@ -1038,6 +1065,10 @@
         pw.println("  mStartingQuickSwitchRotation=" + mStartingQuickSwitchRotation);
         pw.println("  mCurrentRotation=" + mCurrentRotation);
         pw.println("  mHomeButtonLongPressDurationMs=" + mHomeButtonLongPressDurationMs);
+        pw.println("  mOverrideHomeButtonLongPressDurationMs="
+                + mOverrideHomeButtonLongPressDurationMs);
+        pw.println("  mOverrideHomeButtonLongPressSlopMultiplier="
+                + mOverrideHomeButtonLongPressSlopMultiplier);
         pw.println("  mLongPressHomeEnabled=" + mLongPressHomeEnabled);
         pw.println("  mNavigationBarWindowState="
                 + windowStateToString(mNavigationBarWindowState));
@@ -1331,6 +1362,10 @@
         final Optional<CentralSurfaces> centralSurfacesOptional = mCentralSurfacesOptionalLazy.get();
         switch (event.getAction()) {
             case MotionEvent.ACTION_DOWN:
+                if (mCurrentDownEvent != null) {
+                    mCurrentDownEvent.recycle();
+                }
+                mCurrentDownEvent = MotionEvent.obtain(event);
                 mHomeBlockedThisTouch = false;
                 if (mTelecomManagerOptional.isPresent()
                         && mTelecomManagerOptional.get().isRinging()) {
@@ -1342,9 +1377,45 @@
                     }
                 }
                 if (mLongPressHomeEnabled) {
-                    mHomeButtonLongPressDurationMs.ifPresent(longPressDuration -> {
-                        mHandler.postDelayed(mOnVariableDurationHomeLongClick, longPressDuration);
-                    });
+                    if (mOverrideHomeButtonLongPressDurationMs.isPresent()) {
+                        Log.d(TAG, "ACTION_DOWN Launcher override duration: "
+                                + mOverrideHomeButtonLongPressDurationMs.get());
+                        mHandler.postDelayed(mOnVariableDurationHomeLongClick,
+                                mOverrideHomeButtonLongPressDurationMs.get());
+                    } else if (mOverrideHomeButtonLongPressSlopMultiplier.isPresent()) {
+                        // If override timeout doesn't exist but override touch slop exists, we use
+                        // system default long press duration
+                        Log.d(TAG, "ACTION_DOWN default duration: "
+                                + ViewConfiguration.getLongPressTimeout());
+                        mHandler.postDelayed(mOnVariableDurationHomeLongClick,
+                                ViewConfiguration.getLongPressTimeout());
+                    } else {
+                        mHomeButtonLongPressDurationMs.ifPresent(longPressDuration -> {
+                            Log.d(TAG, "ACTION_DOWN original duration: " + longPressDuration);
+                            mHandler.postDelayed(mOnVariableDurationHomeLongClick,
+                                    longPressDuration);
+                        });
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_MOVE:
+                if (!mHandler.hasCallbacks(mOnVariableDurationHomeLongClick)) {
+                    Log.w(TAG, "No callback. Don't handle touch slop.");
+                    break;
+                }
+                float customSlopMultiplier = mOverrideHomeButtonLongPressSlopMultiplier.orElse(1f);
+                float touchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
+                float calculatedTouchSlop =
+                        customSlopMultiplier * QUICKSTEP_TOUCH_SLOP_RATIO_TWO_BUTTON * touchSlop;
+                float touchSlopSquared = calculatedTouchSlop * calculatedTouchSlop;
+
+                float dx = event.getX() - mCurrentDownEvent.getX();
+                float dy = event.getY() - mCurrentDownEvent.getY();
+                double distanceSquared = (dx * dx) + (dy * dy);
+                if (distanceSquared > touchSlopSquared) {
+                    Log.i(TAG, "Touch slop passed. Abort.");
+                    mView.abortCurrentGesture();
+                    mHandler.removeCallbacks(mOnVariableDurationHomeLongClick);
                 }
                 break;
             case MotionEvent.ACTION_UP:
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
index 6710504..8d5aeab5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
@@ -93,6 +93,9 @@
     val isQsFullyCollapsed: Boolean
         get() = true
 
+    /** Request that the customizer be closed. Possibly animating it. */
+    fun requestCloseCustomizer()
+
     sealed interface State {
 
         val isVisible: Boolean
@@ -277,6 +280,10 @@
         bottomNavBarSize.emit(padding)
     }
 
+    override fun requestCloseCustomizer() {
+        qsImpl.value?.closeCustomizer()
+    }
+
     private fun QSImpl.applyState(state: QSSceneAdapter.State) {
         setQsVisible(state.isVisible)
         setExpanded(state.isVisible && state.expansion > 0f)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
index c695d4c..62ed491 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
@@ -20,7 +20,6 @@
 import com.android.compose.animation.scene.Back
 import com.android.compose.animation.scene.Swipe
 import com.android.compose.animation.scene.SwipeDirection
-import com.android.compose.animation.scene.UserAction
 import com.android.compose.animation.scene.UserActionResult
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.qs.FooterActionsController
@@ -47,7 +46,9 @@
     val destinationScenes =
         qsSceneAdapter.isCustomizing.map { customizing ->
             if (customizing) {
-                mapOf<UserAction, UserActionResult>(Back to UserActionResult(Scenes.QuickSettings))
+                // TODO(b/332749288) Empty map so there are no back handlers and back can close
+                // customizer
+                emptyMap()
                 // TODO(b/330200163) Add an Up from Bottom to be able to collapse the shade
                 // while customizing
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 7c1a2c0..f621f11 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -259,6 +259,12 @@
         }
 
         @Override
+        public void setOverrideHomeButtonLongPress(long duration, float slopMultiplier) {
+            verifyCallerAndClearCallingIdentityPostMain("setOverrideHomeButtonLongPress",
+                    () -> notifySetOverrideHomeButtonLongPress(duration, slopMultiplier));
+        }
+
+        @Override
         public void onBackPressed() {
             verifyCallerAndClearCallingIdentityPostMain("onBackPressed", () -> {
                 sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK);
@@ -947,6 +953,12 @@
         }
     }
 
+    private void notifySetOverrideHomeButtonLongPress(long duration, float slopMultiplier) {
+        for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
+            mConnectionCallbacks.get(i).setOverrideHomeButtonLongPress(duration, slopMultiplier);
+        }
+    }
+
     public void notifyAssistantVisibilityChanged(float visibility) {
         try {
             if (mOverviewProxy != null) {
@@ -1104,6 +1116,8 @@
         default void startAssistant(Bundle bundle) {}
         default void setAssistantOverridesRequested(int[] invocationTypes) {}
         default void animateNavBarLongPress(boolean isTouchDown, boolean shrink, long durationMs) {}
+        /** Set override of home button long press duration and touch slop multiplier. */
+        default void setOverrideHomeButtonLongPress(long override, float slopMultiplier) {}
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt
index 9e27dad..5664d59 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.shared.ComposeLockscreen
 import com.android.systemui.media.controls.util.MediaInSceneContainerFlag
+import com.android.systemui.statusbar.phone.PredictiveBackSysUiFlag
 import dagger.Module
 import dagger.Provides
 
@@ -44,7 +45,8 @@
                 MigrateClocksToBlueprint.isEnabled &&
                 ComposeLockscreen.isEnabled &&
                 MediaInSceneContainerFlag.isEnabled &&
-                KeyguardWmStateRefactor.isEnabled
+                KeyguardWmStateRefactor.isEnabled &&
+                PredictiveBackSysUiFlag.isEnabled
     // NOTE: Changes should also be made in getSecondaryFlags and @EnableSceneContainer
 
     /** The main aconfig flag. */
diff --git a/packages/SystemUI/src/com/android/systemui/shade/DebugDrawable.java b/packages/SystemUI/src/com/android/systemui/shade/DebugDrawable.java
index c42fdf8..b24edd9 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/DebugDrawable.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/DebugDrawable.java
@@ -25,6 +25,7 @@
 import android.graphics.drawable.Drawable;
 
 import com.android.keyguard.LockIconViewController;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
 
 import java.util.HashSet;
@@ -80,12 +81,14 @@
                         mNotificationPanelViewController.getClockPositionResult()
                                 .stackScrollerPadding),
                 Color.YELLOW, "calculatePanelHeightShade()");
-        drawDebugInfo(canvas,
-                (int) mQsController.calculateNotificationsTopPadding(
-                        mNotificationPanelViewController.isExpandingOrCollapsing(),
-                        mNotificationPanelViewController.getKeyguardNotificationStaticPadding(),
-                        mNotificationPanelViewController.getExpandedFraction()),
-                Color.MAGENTA, "calculateNotificationsTopPadding()");
+        if (!SceneContainerFlag.isEnabled()) {
+            drawDebugInfo(canvas,
+                    (int) mQsController.calculateNotificationsTopPadding(
+                            mNotificationPanelViewController.isExpandingOrCollapsing(),
+                            mNotificationPanelViewController.getKeyguardNotificationStaticPadding(),
+                            mNotificationPanelViewController.getExpandedFraction()),
+                    Color.MAGENTA, "calculateNotificationsTopPadding()");
+        }
         drawDebugInfo(canvas, mNotificationPanelViewController.getClockPositionResult().clockY,
                 Color.GRAY, "mClockPositionResult.clockY");
         drawDebugInfo(canvas, (int) mLockIconViewController.getTop(), Color.GRAY,
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 343f377..4660831 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -1588,7 +1588,8 @@
      * @param forceClockUpdate Should the clock be updated even when not on keyguard
      */
     private void positionClockAndNotifications(boolean forceClockUpdate) {
-        boolean animate = mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending();
+        boolean animate = !SceneContainerFlag.isEnabled()
+                && mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending();
         int stackScrollerPadding;
         boolean onKeyguard = isKeyguardShowing();
 
@@ -1675,7 +1676,8 @@
                 mClockPositionResult.clockX, mClockPositionResult.clockY);
         }
 
-        boolean animate = mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending();
+        boolean animate = !SceneContainerFlag.isEnabled()
+                && mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending();
         boolean animateClock = (animate || mAnimateNextPositionUpdate) && shouldAnimateClockChange;
 
         if (!MigrateClocksToBlueprint.isEnabled()) {
@@ -2483,6 +2485,7 @@
 
     /** Returns the topPadding of notifications when on keyguard not respecting QS expansion. */
     int getKeyguardNotificationStaticPadding() {
+        SceneContainerFlag.assertInLegacyMode();
         if (!isKeyguardShowing()) {
             return 0;
         }
@@ -2524,12 +2527,14 @@
     }
 
     void requestScrollerTopPaddingUpdate(boolean animate) {
-        float padding = mQsController.calculateNotificationsTopPadding(mIsExpandingOrCollapsing,
-                getKeyguardNotificationStaticPadding(), mExpandedFraction);
-        if (MigrateClocksToBlueprint.isEnabled()) {
-            mSharedNotificationContainerInteractor.setTopPosition(padding);
-        } else {
-            mNotificationStackScrollLayoutController.updateTopPadding(padding, animate);
+        if (!SceneContainerFlag.isEnabled()) {
+            float padding = mQsController.calculateNotificationsTopPadding(mIsExpandingOrCollapsing,
+                    getKeyguardNotificationStaticPadding(), mExpandedFraction);
+            if (MigrateClocksToBlueprint.isEnabled()) {
+                mSharedNotificationContainerInteractor.setTopPosition(padding);
+            } else {
+                mNotificationStackScrollLayoutController.updateTopPadding(padding, animate);
+            }
         }
 
         if (isKeyguardShowing()
@@ -3174,6 +3179,7 @@
             }
             notifyExpandingFinished();
         }
+        // TODO(b/332732878): replace this call when scene container is enabled
         mNotificationStackScrollLayoutController.setAnimationsEnabled(!disabled);
     }
 
@@ -3963,7 +3969,9 @@
             mShadeRepository.setLegacyShadeExpansion(mExpandedFraction);
             mQsController.setShadeExpansion(mExpandedHeight, mExpandedFraction);
             mExpansionDragDownAmountPx = h;
-            mAmbientState.setExpansionFraction(mExpandedFraction);
+            if (!SceneContainerFlag.isEnabled()) {
+                mAmbientState.setExpansionFraction(mExpandedFraction);
+            }
             onHeightUpdated(mExpandedHeight);
             updateExpansionAndVisibility();
         });
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index 89e8413..fb32b9f 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -417,6 +417,12 @@
         } else {
             mLpChanged.flags &= ~LayoutParams.FLAG_SECURE;
         }
+
+        if (state.bouncerShowing) {
+            mLpChanged.inputFeatures |= LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING;
+        } else {
+            mLpChanged.inputFeatures &= ~LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING;
+        }
     }
 
     protected boolean isDebuggable() {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
index 35b4059..2507507 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
@@ -1371,6 +1371,7 @@
     @Override
     public float calculateNotificationsTopPadding(boolean isShadeExpanding,
             int keyguardNotificationStaticPadding, float expandedFraction) {
+        SceneContainerFlag.assertInLegacyMode();
         float topPadding;
         boolean keyguardShowing = mBarState == KEYGUARD;
         if (mSplitShadeEnabled) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerSceneImpl.kt
index b8250cc..3462993 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerSceneImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerSceneImpl.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.shade
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.plugins.qs.QSContainerController
 import com.android.systemui.qs.ui.adapter.QSSceneAdapter
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import javax.inject.Inject
@@ -28,7 +27,6 @@
 constructor(
     private val shadeInteractor: ShadeInteractor,
     private val qsSceneAdapter: QSSceneAdapter,
-    private val qsContainerController: QSContainerController,
 ) : QuickSettingsController {
 
     override val expanded: Boolean
@@ -43,7 +41,7 @@
     }
 
     override fun closeQsCustomizer() {
-        qsContainerController.setCustomizerShowing(false)
+        qsSceneAdapter.requestCloseCustomizer()
     }
 
     @Deprecated("specific to legacy split shade")
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
index c5e07e8..ebebbe6 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.dagger.ShadeTouchLog
 import com.android.systemui.scene.domain.interactor.SceneInteractor
@@ -61,6 +62,7 @@
     private val shadeInteractor: ShadeInteractor,
     private val sceneInteractor: SceneInteractor,
     private val deviceEntryInteractor: DeviceEntryInteractor,
+    private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
     private val notificationStackScrollLayout: NotificationStackScrollLayout,
     @ShadeTouchLog private val touchLog: LogBuffer,
     private val vibratorHelper: VibratorHelper,
@@ -148,7 +150,11 @@
     }
 
     private fun getCollapseDestinationScene(): SceneKey {
-        return if (deviceEntryInteractor.isDeviceEntered.value) {
+        // Always check whether device is unlocked before transitioning to gone scene.
+        return if (
+            deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked &&
+                deviceEntryInteractor.isDeviceEntered.value
+        ) {
             Scenes.Gone
         } else {
             Scenes.Lockscreen
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeSurfaceImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeSurfaceImpl.kt
index adb2928..ec4018c 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeSurfaceImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeSurfaceImpl.kt
@@ -46,7 +46,7 @@
     }
 
     override fun setTouchAndAnimationDisabled(disabled: Boolean) {
-        // TODO(b/322197941): determine if still needed
+        // TODO(b/332732878): determine if still needed
     }
 
     override fun setWillPlayDelayedDozeAmountAnimation(willPlay: Boolean) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt
index 2cb9f9a..29cdf0a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt
@@ -43,6 +43,7 @@
 import com.android.systemui.statusbar.LightRevealScrim
 import com.android.systemui.statusbar.NotificationInsetsController
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
+import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import com.android.systemui.statusbar.phone.KeyguardBottomAreaView
 import com.android.systemui.statusbar.phone.StatusBarLocation
@@ -51,6 +52,7 @@
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.tuner.TunerService
+import dagger.Binds
 import dagger.Module
 import dagger.Provides
 import javax.inject.Named
@@ -59,6 +61,14 @@
 /** Module for providing views related to the shade. */
 @Module
 abstract class ShadeViewProviderModule {
+
+    @Binds
+    @SysUISingleton
+    // TODO(b/277762009): Only allow this view's binder to inject the view.
+    abstract fun bindsNotificationScrollView(
+        notificationStackScrollLayout: NotificationStackScrollLayout
+    ): NotificationScrollView
+
     companion object {
         const val SHADE_HEADER = "large_screen_shade_header"
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java
index 3349345..c429329 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java
@@ -345,9 +345,7 @@
             }
         }
 
-        if (mStatusBarPipelineFlags.useNewShadeCarrierGroupMobileIcons()) {
-            Log.d(TAG, "ignoring old pipeline callback because new mobile icon is enabled");
-        } else {
+        if (!mStatusBarPipelineFlags.useNewShadeCarrierGroupMobileIcons()) {
             for (int i = 0; i < SIM_SLOTS; i++) {
                 mCarrierGroups[i].updateState(mInfos[i], singleCarrier);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
index d6858ca..78e108d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
@@ -40,6 +40,7 @@
 import android.os.Looper;
 import android.os.RemoteException;
 import android.text.Editable;
+import android.text.TextUtils;
 import android.text.TextWatcher;
 import android.util.Log;
 import android.util.Pair;
@@ -57,6 +58,7 @@
 import android.view.View.AccessibilityDelegate;
 import android.view.ViewGroup;
 import android.view.Window;
+import android.view.WindowInsets;
 import android.view.WindowManager;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.Button;
@@ -104,6 +106,7 @@
 
     private WindowManager mWindowManager;
     private EditText mSearchEditText;
+    private ImageButton mEditTextCancel;
     private String mQueryString;
     private int mCurrentCategoryIndex = 0;
     private Map<Integer, Boolean> mKeySearchResultMap = new HashMap<>();
@@ -143,7 +146,7 @@
     @VisibleForTesting
     KeyboardShortcutListSearch(Context context, WindowManager windowManager) {
         this.mContext = new ContextThemeWrapper(
-                context, android.R.style.Theme_DeviceDefault_Settings);
+                context, R.style.KeyboardShortcutHelper);
         this.mPackageManager = AppGlobals.getPackageManager();
         if (windowManager != null) {
             this.mWindowManager = windowManager;
@@ -853,13 +856,14 @@
             List<List<KeyboardShortcutMultiMappingGroup>> keyboardShortcutMultiMappingGroupList) {
         mQueryString = null;
         LayoutInflater inflater = mContext.getSystemService(LayoutInflater.class);
-        mKeyboardShortcutsBottomSheetDialog =
-                new BottomSheetDialog(mContext);
+        mKeyboardShortcutsBottomSheetDialog  = new BottomSheetDialog(mContext);
         final View keyboardShortcutsView = inflater.inflate(
                 R.layout.keyboard_shortcuts_search_view, null);
         LinearLayout shortcutsContainer = keyboardShortcutsView.findViewById(
                 R.id.keyboard_shortcuts_container);
         mNoSearchResults = keyboardShortcutsView.findViewById(R.id.shortcut_search_no_result);
+        Window keyboardShortcutsWindow = mKeyboardShortcutsBottomSheetDialog.getWindow();
+        setWindowProperties(keyboardShortcutsWindow);
         mKeyboardShortcutsBottomSheetDialog.setContentView(keyboardShortcutsView);
         setButtonsDefaultStatus(keyboardShortcutsView);
         populateCurrentAppButton();
@@ -874,25 +878,11 @@
         }
 
         BottomSheetBehavior<FrameLayout> behavior = BottomSheetBehavior.from(bottomSheet);
+        behavior.setDraggable(true);
         behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
         behavior.setSkipCollapsed(true);
-        behavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
-                    @Override
-                    public void onStateChanged(@NonNull View bottomSheet, int newState) {
-                        if (newState == BottomSheetBehavior.STATE_DRAGGING) {
-                            behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
-                        }
-                    }
 
-                    @Override
-                    public void onSlide(@NonNull View bottomSheet, float slideOffset) {
-                        // Do nothing.
-                    }
-                });
 
-        mKeyboardShortcutsBottomSheetDialog.setCanceledOnTouchOutside(true);
-        Window keyboardShortcutsWindow = mKeyboardShortcutsBottomSheetDialog.getWindow();
-        keyboardShortcutsWindow.setType(TYPE_SYSTEM_DIALOG);
         synchronized (sLock) {
             // show KeyboardShortcutsBottomSheetDialog only if it has not been dismissed already
             if (sInstance != null) {
@@ -908,6 +898,8 @@
             }
         }
         mSearchEditText = keyboardShortcutsView.findViewById(R.id.keyboard_shortcuts_search);
+        mEditTextCancel = keyboardShortcutsView.findViewById(
+                R.id.keyboard_shortcuts_search_cancel);
         mSearchEditText.addTextChangedListener(
                 new TextWatcher() {
                     @Override
@@ -921,6 +913,8 @@
                             shortcutsContainer.setAccessibilityPaneTitle(mContext.getString(
                                     R.string.keyboard_shortcut_a11y_show_search_results));
                         }
+                        mEditTextCancel.setVisibility(
+                                TextUtils.isEmpty(mQueryString) ? View.GONE : View.VISIBLE);
                     }
 
                     @Override
@@ -933,9 +927,28 @@
                         // Do nothing.
                     }
                 });
-        ImageButton editTextCancel = keyboardShortcutsView.findViewById(
-                R.id.keyboard_shortcuts_search_cancel);
-        editTextCancel.setOnClickListener(v -> mSearchEditText.setText(null));
+
+        mEditTextCancel.setOnClickListener(v -> mSearchEditText.setText(null));
+    }
+
+    private static void setWindowProperties(Window keyboardShortcutsWindow) {
+        keyboardShortcutsWindow.setType(TYPE_SYSTEM_DIALOG);
+        WindowManager.LayoutParams params = new WindowManager.LayoutParams();
+        params.copyFrom(keyboardShortcutsWindow.getAttributes());
+        // Allows the bottom sheet dialog to render all the way to the bottom of the screen,
+        // behind the gesture navigation bar.
+        params.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
+        params.setFitInsetsTypes(WindowInsets.Type.statusBars());
+        keyboardShortcutsWindow.setAttributes(params);
+        keyboardShortcutsWindow.getDecorView().setOnApplyWindowInsetsListener((v, insets) -> {
+            int bottom = insets.getInsets(WindowInsets.Type.navigationBars()).bottom;
+            View container = v.findViewById(R.id.keyboard_shortcuts_container);
+            container.setPadding(container.getPaddingLeft(), container.getPaddingTop(),
+                    container.getPaddingRight(), bottom);
+            return WindowInsets.CONSUMED;
+        });
+        keyboardShortcutsWindow.setWindowAnimations(
+                R.style.KeyboardShortcutHelper_BottomSheetDialogAnimation);
     }
 
     private void populateKeyboardShortcutSearchList(LinearLayout keyboardShortcutsLayout) {
@@ -1256,10 +1269,10 @@
         if (mContext.getResources().getConfiguration().orientation
                 == Configuration.ORIENTATION_PORTRAIT) {
             lp.width = (int) (display.getWidth() * 0.8);
-            lp.height = (int) (display.getHeight() * 0.7);
+            lp.height = (int) (display.getHeight() * 0.8);
         } else {
             lp.width = (int) (display.getWidth() * 0.7);
-            lp.height = (int) (display.getHeight() * 0.8);
+            lp.height = (int) (display.getHeight() * 0.95);
         }
         window.setGravity(Gravity.BOTTOM);
         window.setAttributes(lp);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
index 8ea29dd6..aa6bec1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
@@ -49,7 +49,6 @@
 import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus;
 import com.android.systemui.keyguard.MigrateClocksToBlueprint;
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor;
-import com.android.systemui.plugins.clocks.ClockController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
 import com.android.systemui.res.R;
 import com.android.systemui.scene.domain.interactor.SceneInteractor;
@@ -469,13 +468,7 @@
     /** Returns the id of the currently rendering clock */
     public String getClockId() {
         if (MigrateClocksToBlueprint.isEnabled()) {
-            ClockController clock = mKeyguardClockInteractorLazy.get()
-                    .getCurrentClock().getValue();
-            if (clock == null) {
-                Log.e(TAG, "No clock is available");
-                return KeyguardClockSwitch.MISSING_CLOCK_ID;
-            }
-            return clock.getConfig().getId();
+            return mKeyguardClockInteractorLazy.get().getRenderedClockId();
         }
 
         if (mClockSwitchView == null) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
index 8531eaa..1a223c1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
@@ -18,6 +18,7 @@
 
 package com.android.systemui.statusbar.notification.collection.coordinator
 
+import android.os.SystemProperties
 import android.os.UserHandle
 import android.provider.Settings
 import androidx.annotation.VisibleForTesting
@@ -340,12 +341,41 @@
 
             var hasFilteredAnyNotifs = false
 
+            /**
+             * the [notificationMinimalismPrototype] will now show seen notifications on the locked
+             * shade by default, but this property read allows that to be quickly disabled for
+             * testing
+             */
+            private val minimalismShowOnLockedShade
+                get() =
+                    SystemProperties.getBoolean(
+                        "persist.notification_minimalism_prototype.show_on_locked_shade",
+                        true
+                    )
+
+            /**
+             * Encapsulates a definition of "being on the keyguard". Note that these two definitions
+             * are wildly different: [StatusBarState.KEYGUARD] is when on the lock screen and does
+             * not include shade or occluded states, whereas [KeyguardRepository.isKeyguardShowing]
+             * is any state where the keyguard has not been dismissed, including locked shade and
+             * occluded lock screen.
+             *
+             * Returning false for locked shade and occluded states means that this filter will
+             * allow seen notifications to appear in the locked shade.
+             */
+            private fun isOnKeyguard(): Boolean =
+                if (notificationMinimalismPrototype() && minimalismShowOnLockedShade) {
+                    statusBarStateController.state == StatusBarState.KEYGUARD
+                } else {
+                    keyguardRepository.isKeyguardShowing()
+                }
+
             override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean =
                 when {
                     // Don't apply filter if the setting is disabled
                     !unseenFilterEnabled -> false
                     // Don't apply filter if the keyguard isn't currently showing
-                    !keyguardRepository.isKeyguardShowing() -> false
+                    !isOnKeyguard() -> false
                     // Don't apply the filter if the notification is unseen
                     unseenNotifications.contains(entry) -> false
                     // Don't apply the filter to (non-promoted) group summaries
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index 5b9eb21..0bb871b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -742,7 +742,7 @@
         pw.println("mHideSensitive=" + mHideSensitive);
         pw.println("mShadeExpanded=" + mShadeExpanded);
         pw.println("mClearAllInProgress=" + mClearAllInProgress);
-        pw.println("mStatusBarState=" + mStatusBarState);
+        pw.println("mStatusBarState=" + StatusBarState.toString(mStatusBarState));
         pw.println("mExpansionChanging=" + mExpansionChanging);
         pw.println("mPanelFullWidth=" + mIsSmallScreen);
         pw.println("mPulsing=" + mPulsing);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 3367dc4..fedb88d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -113,6 +113,9 @@
 import com.android.systemui.statusbar.notification.row.StackScrollerDecorView;
 import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation;
 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor;
+import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds;
+import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape;
+import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
 import com.android.systemui.statusbar.phone.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
@@ -135,6 +138,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.function.BiConsumer;
@@ -143,7 +147,9 @@
 /**
  * A layout which handles a dynamic amount of notifications and presents them in a scrollable stack.
  */
-public class NotificationStackScrollLayout extends ViewGroup implements Dumpable {
+public class NotificationStackScrollLayout
+        extends ViewGroup
+        implements Dumpable, NotificationScrollView {
 
     public static final float BACKGROUND_ALPHA_DIMMED = 0.7f;
     private static final String TAG = "StackScroller";
@@ -218,6 +224,7 @@
      */
     private final StackScrollAlgorithm mStackScrollAlgorithm;
     private final AmbientState mAmbientState;
+    private final ScrollViewFields mScrollViewFields = new ScrollViewFields();
 
     private final GroupMembershipManager mGroupMembershipManager;
     private final GroupExpansionManager mGroupExpansionManager;
@@ -230,7 +237,8 @@
     private final ArrayList<View> mSwipedOutViews = new ArrayList<>();
     private NotificationStackSizeCalculator mNotificationStackSizeCalculator;
     private final StackStateAnimator mStateAnimator;
-    private boolean mAnimationsEnabled;
+    // TODO(b/332732878): call setAnimationsEnabled with scene container enabled, then remove this
+    private boolean mAnimationsEnabled = SceneContainerFlag.isEnabled();
     private boolean mChangePositionInProgress;
     private boolean mChildTransferInProgress;
 
@@ -589,7 +597,7 @@
         @Override
         public boolean isScrolledToTop() {
             if (SceneContainerFlag.isEnabled()) {
-                return mController.isPlaceholderScrolledToTop();
+                return mScrollViewFields.isScrolledToTop();
             } else {
                 return mOwnScrollY == 0;
             }
@@ -854,7 +862,8 @@
         drawDebugInfo(canvas, y, Color.MAGENTA,
                 /* label= */ "mContentHeight = " + y);
 
-        drawDebugInfo(canvas, mRoundedRectClippingBottom, Color.DKGRAY,
+        y = mRoundedRectClippingBottom;
+        drawDebugInfo(canvas, y, Color.DKGRAY,
                 /* label= */ "mRoundedRectClippingBottom) = " + y);
     }
 
@@ -1130,6 +1139,48 @@
         }
     }
 
+    @Override
+    public View asView() {
+        return this;
+    }
+
+    @Override
+    public void setScrolledToTop(boolean scrolledToTop) {
+        mScrollViewFields.setScrolledToTop(scrolledToTop);
+    }
+
+    @Override
+    public void setStackTop(float stackTop) {
+        mScrollViewFields.setStackTop(stackTop);
+        // TODO(b/332574413): replace the following with using stackTop
+        updateTopPadding(stackTop, isAddOrRemoveAnimationPending());
+    }
+
+    @Override
+    public void setStackBottom(float stackBottom) {
+        mScrollViewFields.setStackBottom(stackBottom);
+    }
+
+    @Override
+    public void setHeadsUpTop(float headsUpTop) {
+        mScrollViewFields.setHeadsUpTop(headsUpTop);
+    }
+
+    @Override
+    public void setSyntheticScrollConsumer(@Nullable Consumer<Float> consumer) {
+        mScrollViewFields.setSyntheticScrollConsumer(consumer);
+    }
+
+    @Override
+    public void setStackHeightConsumer(@Nullable Consumer<Float> consumer) {
+        mScrollViewFields.setStackHeightConsumer(consumer);
+    }
+
+    @Override
+    public void setHeadsUpHeightConsumer(@Nullable Consumer<Float> consumer) {
+        mScrollViewFields.setHeadsUpHeightConsumer(consumer);
+    }
+
     /**
      * @param listener to be notified after the location of Notification children might have
      *                 changed.
@@ -1380,6 +1431,31 @@
         mOnStackYChanged = onStackYChanged;
     }
 
+    @Override
+    public void setExpandFraction(float expandFraction) {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+        final float oldFraction = mAmbientState.getExpansionFraction();
+        final boolean wasExpanding = oldFraction != 0f && oldFraction != 1f;
+        final boolean nowExpanding = expandFraction != 0f && expandFraction != 1f;
+
+        // need to enter 'expanding' state before handling the new expand fraction, and then
+        if (nowExpanding && !wasExpanding) {
+            onExpansionStarted();
+            mController.checkSnoozeLeavebehind();
+        }
+
+        // Update the expand progress between started/stopped events
+        mAmbientState.setExpansionFraction(expandFraction);
+        // TODO(b/332577544): don't convert to height which then converts to the fraction again
+        setExpandedHeight(expandFraction * getHeight());
+
+        // expansion stopped event requires that the expandFraction has already been updated
+        if (!nowExpanding && wasExpanding) {
+            setCheckForLeaveBehind(false);
+            onExpansionStopped();
+        }
+    }
+
     /**
      * Update the height of the panel.
      *
@@ -2314,7 +2390,7 @@
                         /* notificationStackScrollLayout= */ this, mMaxDisplayedNotifications,
                         shelfIntrinsicHeight);
         mIntrinsicContentHeight = height;
-        mController.setIntrinsicContentHeight(mIntrinsicContentHeight);
+        mScrollViewFields.sendStackHeight(height);
 
         // The topPadding can be bigger than the regular padding when qs is expanded, in that
         // state the maxPanelHeight and the contentHeight should be bigger
@@ -2904,6 +2980,7 @@
     }
 
     public void setAnimationsEnabled(boolean animationsEnabled) {
+        // TODO(b/332732878): remove the initial value of this field once the setter is called
         mAnimationsEnabled = animationsEnabled;
         updateNotificationAnimationStates();
         if (!animationsEnabled) {
@@ -3553,7 +3630,7 @@
 
     protected boolean isInsideQsHeader(MotionEvent ev) {
         if (SceneContainerFlag.isEnabled()) {
-            return ev.getY() < mController.getPlaceholderTop();
+            return ev.getY() < mScrollViewFields.getScrimClippingShape().getBounds().getTop();
         }
 
         mQsHeader.getBoundsOnScreen(mQsHeaderBound);
@@ -4086,7 +4163,7 @@
                     // to it so that it can scroll the stack and scrim accordingly.
                     if (SceneContainerFlag.isEnabled()) {
                         float diff = endPosition - layoutEnd;
-                        mController.sendSyntheticScrollToSceneFramework(diff);
+                        mScrollViewFields.sendSyntheticScroll(diff);
                     }
                     setOwnScrollY((int) (mOwnScrollY + endPosition - layoutEnd));
                     mDisallowScrollingInThisMotion = true;
@@ -4969,6 +5046,7 @@
                     elapsedRealtime - mLastUpdateSidePaddingElapsedRealtime);
             println(pw, "isSmallLandscapeLockscreenEnabled", mIsSmallLandscapeLockscreenEnabled);
             mNotificationStackSizeCalculator.dump(pw, args);
+            mScrollViewFields.dump(pw);
         });
         pw.println();
         pw.println("Contents:");
@@ -5509,8 +5587,40 @@
     /**
      * Set rounded rect clipping bounds on this view.
      */
+    @Override
+    public void setScrimClippingShape(@Nullable ShadeScrimShape shape) {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+        if (Objects.equals(mScrollViewFields.getScrimClippingShape(), shape)) return;
+        mScrollViewFields.setScrimClippingShape(shape);
+        mShouldUseRoundedRectClipping = shape != null;
+        mRoundedClipPath.reset();
+        if (shape != null) {
+            ShadeScrimBounds bounds = shape.getBounds();
+            mRoundedRectClippingLeft = (int) bounds.getLeft();
+            mRoundedRectClippingTop = (int) bounds.getTop();
+            mRoundedRectClippingRight = (int) bounds.getRight();
+            mRoundedRectClippingBottom = (int) bounds.getBottom();
+            mBgCornerRadii[0] = shape.getTopRadius();
+            mBgCornerRadii[1] = shape.getTopRadius();
+            mBgCornerRadii[2] = shape.getTopRadius();
+            mBgCornerRadii[3] = shape.getTopRadius();
+            mBgCornerRadii[4] = shape.getBottomRadius();
+            mBgCornerRadii[5] = shape.getBottomRadius();
+            mBgCornerRadii[6] = shape.getBottomRadius();
+            mBgCornerRadii[7] = shape.getBottomRadius();
+            mRoundedClipPath.addRoundRect(
+                    bounds.getLeft(), bounds.getTop(), bounds.getRight(), bounds.getBottom(),
+                    mBgCornerRadii, Path.Direction.CW);
+        }
+        invalidate();
+    }
+
+    /**
+     * Set rounded rect clipping bounds on this view.
+     */
     public void setRoundedClippingBounds(int left, int top, int right, int bottom, int topRadius,
                                          int bottomRadius) {
+        SceneContainerFlag.assertInLegacyMode();
         if (mRoundedRectClippingLeft == left && mRoundedRectClippingRight == right
                 && mRoundedRectClippingBottom == bottom && mRoundedRectClippingTop == top
                 && mBgCornerRadii[0] == topRadius && mBgCornerRadii[5] == bottomRadius) {
@@ -5588,6 +5698,7 @@
      * Should we use rounded rect clipping
      */
     private void updateUseRoundedRectClipping() {
+        if (SceneContainerFlag.isEnabled()) return;
         // We don't want to clip notifications when QS is expanded, because incoming heads up on
         // the bottom would be clipped otherwise
         boolean qsAllowsClipping = mQsExpansionFraction < 0.5f || mShouldUseSplitNotificationShade;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 59901ac..06479e5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -83,6 +83,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.power.domain.interactor.PowerInteractor;
 import com.android.systemui.res.R;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
 import com.android.systemui.scene.ui.view.WindowRootView;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.shade.ShadeViewController;
@@ -125,7 +126,6 @@
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
 import com.android.systemui.statusbar.notification.row.NotificationSnooze;
 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor;
-import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor;
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
 import com.android.systemui.statusbar.phone.HeadsUpTouchHelper;
@@ -189,7 +189,6 @@
     private final VisibilityLocationProviderDelegator mVisibilityLocationProviderDelegator;
     private final ShadeController mShadeController;
     private final Provider<WindowRootView> mWindowRootView;
-    private final NotificationStackAppearanceInteractor mStackAppearanceInteractor;
     private final KeyguardMediaController mKeyguardMediaController;
     private final SysuiStatusBarStateController mStatusBarStateController;
     private final KeyguardBypassController mKeyguardBypassController;
@@ -742,7 +741,6 @@
             NotificationListViewBinder viewBinder,
             ShadeController shadeController,
             Provider<WindowRootView> windowRootView,
-            NotificationStackAppearanceInteractor stackAppearanceInteractor,
             InteractionJankMonitor jankMonitor,
             StackStateLogger stackLogger,
             NotificationStackScrollLogger logger,
@@ -795,7 +793,6 @@
         mSeenNotificationsInteractor = seenNotificationsInteractor;
         mShadeController = shadeController;
         mWindowRootView = windowRootView;
-        mStackAppearanceInteractor = stackAppearanceInteractor;
         mFeatureFlags = featureFlags;
         mNotificationTargetsHelper = notificationTargetsHelper;
         mSecureSettings = secureSettings;
@@ -1124,6 +1121,7 @@
     }
 
     public boolean isAddOrRemoveAnimationPending() {
+        SceneContainerFlag.assertInLegacyMode();
         return mView != null && mView.isAddOrRemoveAnimationPending();
     }
 
@@ -1163,29 +1161,6 @@
         }
     }
 
-    /** Send internal notification expansion to the scene container framework. */
-    public void sendSyntheticScrollToSceneFramework(Float delta) {
-        mStackAppearanceInteractor.setSyntheticScroll(delta);
-    }
-
-    /** Get the y-coordinate of the top bound of the stack. */
-    public float getPlaceholderTop() {
-        return mStackAppearanceInteractor.getShadeScrimBounds().getValue().getTop();
-    }
-
-    /**
-     * Returns whether the notification stack is scrolled to the top; i.e., it cannot be scrolled
-     * down any further.
-     */
-    public boolean isPlaceholderScrolledToTop() {
-        return mStackAppearanceInteractor.getScrolledToTop().getValue();
-    }
-
-    /** Set the intrinsic height of the stack content without additional padding. */
-    public void setIntrinsicContentHeight(float intrinsicContentHeight) {
-        mStackAppearanceInteractor.setStackHeight(intrinsicContentHeight);
-    }
-
     public void setIntrinsicPadding(int intrinsicPadding) {
         mView.setIntrinsicPadding(intrinsicPadding);
     }
@@ -1264,6 +1239,7 @@
     }
 
     public void setScrollingEnabled(boolean enabled) {
+        SceneContainerFlag.assertInLegacyMode();
         mView.setScrollingEnabled(enabled);
     }
 
@@ -1284,6 +1260,7 @@
     }
 
     public void updateTopPadding(float qsHeight, boolean animate) {
+        SceneContainerFlag.assertInLegacyMode();
         mView.updateTopPadding(qsHeight, animate);
     }
 
@@ -1386,11 +1363,13 @@
     }
 
     public void onExpansionStarted() {
+        SceneContainerFlag.assertInLegacyMode();
         mView.onExpansionStarted();
         checkSnoozeLeavebehind();
     }
 
     public void onExpansionStopped() {
+        SceneContainerFlag.assertInLegacyMode();
         mView.setCheckForLeaveBehind(false);
         mView.onExpansionStopped();
     }
@@ -1519,6 +1498,7 @@
     }
 
     public void setExpandedHeight(float expandedHeight) {
+        SceneContainerFlag.assertInLegacyMode();
         mView.setExpandedHeight(expandedHeight);
     }
 
@@ -1794,6 +1774,7 @@
      */
     public void setRoundedClippingBounds(int left, int top, int right, int bottom, int topRadius,
             int bottomRadius) {
+        SceneContainerFlag.assertInLegacyMode();
         mView.setRoundedClippingBounds(left, top, right, bottom, topRadius, bottomRadius);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
index 1b53cbe..5bd4c75 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
@@ -17,14 +17,15 @@
 package com.android.systemui.statusbar.notification.stack
 
 import android.content.res.Resources
+import android.os.SystemProperties
 import android.util.Log
 import android.view.View.GONE
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.Flags.notificationMinimalismPrototype
-import com.android.systemui.res.R
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
 import com.android.systemui.statusbar.StatusBarState.KEYGUARD
 import com.android.systemui.statusbar.SysuiStatusBarStateController
@@ -177,8 +178,8 @@
 
         // TODO: Avoid making this split shade assumption by simply checking the stack for media
         val isMediaShowing = mediaDataManager.hasActiveMediaOrRecommendation()
-        val isMediaShowingInStack = isMediaShowing && !splitShadeStateController
-                .shouldUseSplitNotificationShade(resources)
+        val isMediaShowingInStack =
+            isMediaShowing && !splitShadeStateController.shouldUseSplitNotificationShade(resources)
 
         log { "\tGet maxNotifWithoutSavingSpace ---" }
         val maxNotifWithoutSavingSpace =
@@ -378,8 +379,17 @@
     }
 
     fun updateResources() {
-        maxKeyguardNotifications = if (notificationMinimalismPrototype()) 1
-            else infiniteIfNegative(resources.getInteger(R.integer.keyguard_max_notification_count))
+        maxKeyguardNotifications =
+            infiniteIfNegative(
+                if (notificationMinimalismPrototype()) {
+                    SystemProperties.getInt(
+                        "persist.notification_minimalism_prototype.lock_screen_max_notifs",
+                        1
+                    )
+                } else {
+                    resources.getInteger(R.integer.keyguard_max_notification_count)
+                }
+            )
         maxNotificationsExcludesMedia = notificationMinimalismPrototype()
 
         dividerHeight =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
new file mode 100644
index 0000000..edac5ed
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack
+
+import android.util.IndentingPrintWriter
+import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape
+import com.android.systemui.util.printSection
+import com.android.systemui.util.println
+import java.util.function.Consumer
+
+/**
+ * This is a state holder object used by [NSSL][NotificationStackScrollLayout] to contain states
+ * provided by the `NotificationScrollViewBinder` to the `NotificationScrollView`.
+ *
+ * Unlike AmbientState, no class other than NSSL should ever have access to this class in any way.
+ * These fields are effectively NSSL's private fields.
+ */
+class ScrollViewFields {
+    /** Used to produce the clipping path */
+    var scrimClippingShape: ShadeScrimShape? = null
+    /** Y coordinate in view pixels of the top of the notification stack */
+    var stackTop: Float = 0f
+    /**
+     * Y coordinate in view pixels above which the bottom of the notification stack / shelf / footer
+     * must be.
+     */
+    var stackBottom: Float = 0f
+    /** Y coordinate in view pixels of the top of the HUN */
+    var headsUpTop: Float = 0f
+    /** Whether the notifications are scrolled all the way to the top (i.e. when freshly opened) */
+    var isScrolledToTop: Boolean = true
+
+    /**
+     * When internal NSSL expansion requires the stack to be scrolled (e.g. to keep an expanding
+     * notification in view), that scroll amount can be sent here and it will be handled by the
+     * placeholder
+     */
+    var syntheticScrollConsumer: Consumer<Float>? = null
+    /**
+     * Any time the stack height is recalculated, it should be updated here to be used by the
+     * placeholder
+     */
+    var stackHeightConsumer: Consumer<Float>? = null
+    /**
+     * Any time the heads up height is recalculated, it should be updated here to be used by the
+     * placeholder
+     */
+    var headsUpHeightConsumer: Consumer<Float>? = null
+
+    /** send the [syntheticScroll] to the [syntheticScrollConsumer], if present. */
+    fun sendSyntheticScroll(syntheticScroll: Float) =
+        syntheticScrollConsumer?.accept(syntheticScroll)
+    /** send the [stackHeight] to the [stackHeightConsumer], if present. */
+    fun sendStackHeight(stackHeight: Float) = stackHeightConsumer?.accept(stackHeight)
+    /** send the [headsUpHeight] to the [headsUpHeightConsumer], if present. */
+    fun sendHeadsUpHeight(headsUpHeight: Float) = headsUpHeightConsumer?.accept(headsUpHeight)
+
+    fun dump(pw: IndentingPrintWriter) {
+        pw.printSection("StackViewStates") {
+            pw.println("scrimClippingShape", scrimClippingShape)
+            pw.println("stackTop", stackTop)
+            pw.println("stackBottom", stackBottom)
+            pw.println("headsUpTop", headsUpTop)
+            pw.println("isScrolledToTop", isScrolledToTop)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt
index 462547e..b047379 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt
@@ -27,8 +27,12 @@
  */
 @SysUISingleton
 class NotificationPlaceholderRepository @Inject constructor() {
-    /** The bounds of the notification shade scrim / container in the current scene. */
-    val shadeScrimBounds = MutableStateFlow(ShadeScrimBounds())
+    /**
+     * The bounds of the notification shade scrim / container in the current scene.
+     *
+     * When `null`, clipping should not be applied to notifications.
+     */
+    val shadeScrimBounds = MutableStateFlow<ShadeScrimBounds?>(null)
 
     /**
      * The y-coordinate in px of top of the contents of the notification stack. This value can be
@@ -44,7 +48,7 @@
     val headsUpTop = MutableStateFlow(0f)
 
     /** height made available to the notifications in the size-constrained mode of lock screen. */
-    val constrainedAvailableSpace = MutableStateFlow(0f)
+    val constrainedAvailableSpace = MutableStateFlow(0)
 
     /**
      * Whether the notification stack is scrolled to the top; i.e., it cannot be scrolled down any
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt
index 938e43e..8a9da69 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt
@@ -35,7 +35,7 @@
     val stackHeight = MutableStateFlow(0f)
 
     /** The height in px of the current heads up notification. */
-    val activeHeadsUpRowHeight = MutableStateFlow(0f)
+    val headsUpHeight = MutableStateFlow(0f)
 
     /**
      * The amount in px that the notification stack should scroll due to internal expansion. This
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 32562f1..a5b4f5f 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
@@ -42,7 +42,7 @@
     shadeInteractor: ShadeInteractor,
 ) {
     /** The bounds of the notification stack in the current scene. */
-    val shadeScrimBounds: StateFlow<ShadeScrimBounds> =
+    val shadeScrimBounds: StateFlow<ShadeScrimBounds?> =
         placeholderRepository.shadeScrimBounds.asStateFlow()
 
     /**
@@ -59,8 +59,8 @@
                 isExpandingFromHeadsUp,
             ) { shadeMode, isExpandingFromHeadsUp ->
                 ShadeScrimRounding(
-                    roundTop = !(shadeMode == ShadeMode.Split && isExpandingFromHeadsUp),
-                    roundBottom = shadeMode != ShadeMode.Single,
+                    isTopRounded = !(shadeMode == ShadeMode.Split && isExpandingFromHeadsUp),
+                    isBottomRounded = shadeMode != ShadeMode.Single,
                 )
             }
             .distinctUntilChanged()
@@ -72,15 +72,28 @@
      */
     val stackHeight: StateFlow<Float> = viewHeightRepository.stackHeight.asStateFlow()
 
+    /** The height in px of the contents of the HUN. */
+    val headsUpHeight: StateFlow<Float> = viewHeightRepository.headsUpHeight.asStateFlow()
+
     /** The y-coordinate in px of top of the contents of the notification stack. */
     val stackTop: StateFlow<Float> = placeholderRepository.stackTop.asStateFlow()
 
+    /** The y-coordinate in px of bottom of the contents of the notification stack. */
+    val stackBottom: StateFlow<Float> = placeholderRepository.stackBottom.asStateFlow()
+
+    /** The height of the keyguard's available space bounds */
+    val constrainedAvailableSpace: StateFlow<Int> =
+        placeholderRepository.constrainedAvailableSpace.asStateFlow()
+
     /**
      * Whether the notification stack is scrolled to the top; i.e., it cannot be scrolled down any
      * further.
      */
     val scrolledToTop: StateFlow<Boolean> = placeholderRepository.scrolledToTop.asStateFlow()
 
+    /** The y-coordinate in px of bottom of the contents of the HUN. */
+    val headsUpTop: StateFlow<Float> = placeholderRepository.headsUpTop.asStateFlow()
+
     /**
      * The amount in px that the notification stack should scroll due to internal expansion. This
      * should only happen when a notification expansion hits the bottom of the screen, so it is
@@ -89,8 +102,8 @@
     val syntheticScroll: Flow<Float> = viewHeightRepository.syntheticScroll.asStateFlow()
 
     /** Sets the position of the notification stack in the current scene. */
-    fun setShadeScrimBounds(bounds: ShadeScrimBounds) {
-        check(bounds.top <= bounds.bottom) { "Invalid bounds: $bounds" }
+    fun setShadeScrimBounds(bounds: ShadeScrimBounds?) {
+        check(bounds == null || bounds.top <= bounds.bottom) { "Invalid bounds: $bounds" }
         placeholderRepository.shadeScrimBounds.value = bounds
     }
 
@@ -99,9 +112,19 @@
         viewHeightRepository.stackHeight.value = height
     }
 
+    /** Sets the height of heads up notification. */
+    fun setHeadsUpHeight(height: Float) {
+        viewHeightRepository.headsUpHeight.value = height
+    }
+
     /** Sets the y-coord in px of the top of the contents of the notification stack. */
-    fun setStackTop(startY: Float) {
-        placeholderRepository.stackTop.value = startY
+    fun setStackTop(stackTop: Float) {
+        placeholderRepository.stackTop.value = stackTop
+    }
+
+    /** Sets the y-coord in px of the bottom of the contents of the notification stack. */
+    fun setStackBottom(stackBottom: Float) {
+        placeholderRepository.stackBottom.value = stackBottom
     }
 
     /** Sets whether the notification stack is scrolled to the top. */
@@ -113,4 +136,12 @@
     fun setSyntheticScroll(delta: Float) {
         viewHeightRepository.syntheticScroll.value = delta
     }
+
+    fun setConstrainedAvailableSpace(height: Int) {
+        placeholderRepository.constrainedAvailableSpace.value = height
+    }
+
+    fun setHeadsUpTop(headsUpTop: Float) {
+        placeholderRepository.headsUpTop.value = headsUpTop
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimBounds.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimBounds.kt
index 448127a..7e3e724 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimBounds.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimBounds.kt
@@ -29,4 +29,12 @@
 ) {
     /** The current height of the notification container. */
     val height: Float = bottom - top
+
+    operator fun minus(position: ViewPosition) =
+        ShadeScrimBounds(
+            left = left - position.left,
+            top = top - position.top,
+            right = right - position.left,
+            bottom = bottom - position.top,
+        )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimClipping.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimClipping.kt
index 621dd0c..e86fd1c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimClipping.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimClipping.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.statusbar.notification.stack.shared.model
 
-/** Models the clipping rounded rectangle of the notification stack */
+/** Models the clipping rounded rectangle of the notification stack, where the rounding is binary */
 data class ShadeScrimClipping(
     val bounds: ShadeScrimBounds = ShadeScrimBounds(),
     val rounding: ShadeScrimRounding = ShadeScrimRounding()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimRounding.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimRounding.kt
index 2fe265f..9d4231c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimRounding.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimRounding.kt
@@ -19,7 +19,7 @@
 /** Models the corner rounds of the notification stack. */
 data class ShadeScrimRounding(
     /** Whether the top corners of the notification stack should be rounded. */
-    val roundTop: Boolean = false,
+    val isTopRounded: Boolean = false,
     /** Whether the bottom corners of the notification stack should be rounded. */
-    val roundBottom: Boolean = false,
+    val isBottomRounded: Boolean = false,
 )
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallableFactory.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimShape.kt
similarity index 60%
copy from tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallableFactory.java
copy to packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimShape.kt
index b8f9f0e..e6f0647 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallableFactory.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimShape.kt
@@ -14,16 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.asllib;
+package com.android.systemui.statusbar.notification.stack.shared.model
 
-import com.android.asllib.util.MalformedXmlException;
-
-import org.w3c.dom.Element;
-
-import java.util.List;
-
-public interface AslMarshallableFactory<T extends AslMarshallable> {
-
-    /** Creates an {@link AslMarshallableFactory} from human-readable DOM element */
-    T createFromHrElements(List<Element> elements) throws MalformedXmlException;
-}
+/** Models the clipping rounded rectangle of the notification stack, with corner radii in px */
+data class ShadeScrimShape(
+    val bounds: ShadeScrimBounds = ShadeScrimBounds(),
+    /** radius in px of the top corners */
+    val topRadius: Int,
+    /** radius in px of the bottom corners */
+    val bottomRadius: Int,
+)
diff --git a/tools/app_metadata_bundles/src/test/java/com/android/aslgen/AllTests.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ViewPosition.kt
similarity index 69%
rename from tools/app_metadata_bundles/src/test/java/com/android/aslgen/AllTests.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ViewPosition.kt
index 7ebb7a1..5d2b0ad2 100644
--- a/tools/app_metadata_bundles/src/test/java/com/android/aslgen/AllTests.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ViewPosition.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 The Android Open Source Project
+ * 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.
@@ -14,13 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.aslgen;
+package com.android.systemui.statusbar.notification.stack.shared.model
 
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
-
-@RunWith(Suite.class)
-@Suite.SuiteClasses({
-    AslgenTests.class,
-})
-public class AllTests {}
+/** An offset of view, used to adjust bounds. */
+data class ViewPosition(val left: Int = 0, val top: Int = 0)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
new file mode 100644
index 0000000..ac00d3b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.ui.view
+
+import android.view.View
+import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape
+import java.util.function.Consumer
+
+/**
+ * This view (interface) is the view which scrolls and positions the heads up notification and
+ * notification stack, but is otherwise agnostic to the content.
+ */
+interface NotificationScrollView {
+    /**
+     * Since this is an interface rather than a literal View, this provides cast-like access to the
+     * underlying view.
+     */
+    fun asView(): View
+
+    /** Set the clipping bounds used when drawing */
+    fun setScrimClippingShape(shape: ShadeScrimShape?)
+
+    /** set the y position in px of the top of the stack in this view's coordinates */
+    fun setStackTop(stackTop: Float)
+
+    /** set the y position in px of the bottom of the stack in this view's coordinates */
+    fun setStackBottom(stackBottom: Float)
+
+    /** set the y position in px of the top of the HUN in this view's coordinates */
+    fun setHeadsUpTop(headsUpTop: Float)
+
+    /** set whether the view has been scrolled all the way to the top */
+    fun setScrolledToTop(scrolledToTop: Boolean)
+
+    /** Set a consumer for synthetic scroll events */
+    fun setSyntheticScrollConsumer(consumer: Consumer<Float>?)
+    /** Set a consumer for stack height changed events */
+    fun setStackHeightConsumer(consumer: Consumer<Float>?)
+    /** Set a consumer for heads up height changed events */
+    fun setHeadsUpHeightConsumer(consumer: Consumer<Float>?)
+
+    /** sets that scrolling is allowed */
+    fun setScrollingEnabled(enabled: Boolean)
+
+    /** sets the current expand fraction */
+    fun setExpandFraction(expandFraction: Float)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
new file mode 100644
index 0000000..a98717a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
@@ -0,0 +1,108 @@
+/*
+ * 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.view.View
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.common.ui.ConfigurationState
+import com.android.systemui.common.ui.view.onLayoutChanged
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.notification.stack.shared.model.ViewPosition
+import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationScrollViewModel
+import com.android.systemui.util.kotlin.FlowDumperImpl
+import com.android.systemui.util.kotlin.launchAndDispose
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+/** Binds the [NotificationScrollView]. */
+@SysUISingleton
+class NotificationScrollViewBinder
+@Inject
+constructor(
+    dumpManager: DumpManager,
+    @Main private val mainImmediateDispatcher: CoroutineDispatcher,
+    private val view: NotificationScrollView,
+    private val viewModel: NotificationScrollViewModel,
+    private val configuration: ConfigurationState,
+) : FlowDumperImpl(dumpManager) {
+
+    private val viewPosition = MutableStateFlow(ViewPosition()).dumpValue("viewPosition")
+    private val viewTopOffset = viewPosition.map { it.top }.distinctUntilChanged()
+
+    fun bindWhileAttached(): DisposableHandle {
+        return view.asView().repeatWhenAttached(mainImmediateDispatcher) {
+            repeatOnLifecycle(Lifecycle.State.CREATED) { bind() }
+        }
+    }
+
+    suspend fun bind() = coroutineScope {
+        launchAndDispose {
+            viewPosition.value = view.asView().position
+            view.asView().onLayoutChanged { viewPosition.value = it.position }
+        }
+
+        launch {
+            viewModel.shadeScrimShape(scrimRadius, viewPosition).collect {
+                view.setScrimClippingShape(it)
+            }
+        }
+
+        launch { viewModel.stackTop.minusTopOffset().collect { view.setStackTop(it) } }
+        launch { viewModel.stackBottom.minusTopOffset().collect { view.setStackBottom(it) } }
+        launch { viewModel.scrolledToTop.collect { view.setScrolledToTop(it) } }
+        launch { viewModel.headsUpTop.minusTopOffset().collect { view.setHeadsUpTop(it) } }
+        launch { viewModel.expandFraction.collect { view.setExpandFraction(it) } }
+        launch { viewModel.isScrollable.collect { view.setScrollingEnabled(it) } }
+
+        launchAndDispose {
+            view.setSyntheticScrollConsumer(viewModel.syntheticScrollConsumer)
+            view.setStackHeightConsumer(viewModel.stackHeightConsumer)
+            view.setHeadsUpHeightConsumer(viewModel.headsUpHeightConsumer)
+            DisposableHandle {
+                view.setSyntheticScrollConsumer(null)
+                view.setStackHeightConsumer(null)
+                view.setHeadsUpHeightConsumer(null)
+            }
+        }
+    }
+
+    /** Combine with the topOffset flow and subtract that value from this flow's value */
+    private fun Flow<Float>.minusTopOffset() =
+        combine(viewTopOffset) { y, topOffset -> y - topOffset }
+
+    /** flow of the scrim clipping radius */
+    private val scrimRadius: Flow<Int>
+        get() = configuration.getDimensionPixelOffset(R.dimen.notification_scrim_corner_radius)
+
+    /** Construct a [ViewPosition] from this view using [View.getLeft] and [View.getTop] */
+    private val View.position
+        get() = ViewPosition(left = left, top = top)
+}
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
deleted file mode 100644
index d6d31db..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt
+++ /dev/null
@@ -1,103 +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 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.shadeScrimClipping, 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.stackTop.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 4a096a8..cfd19ba 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
@@ -49,7 +49,7 @@
     private val sceneContainerFlags: SceneContainerFlags,
     private val controller: NotificationStackScrollLayoutController,
     private val notificationStackSizeCalculator: NotificationStackSizeCalculator,
-    private val notificationStackViewBinder: NotificationStackViewBinder,
+    private val notificationScrollViewBinder: NotificationScrollViewBinder,
     @Main private val mainImmediateDispatcher: CoroutineDispatcher,
 ) {
 
@@ -162,7 +162,7 @@
             }
 
         if (sceneContainerFlags.isEnabled()) {
-            disposables += notificationStackViewBinder.bindWhileAttached()
+            disposables += notificationScrollViewBinder.bindWhileAttached()
         }
 
         controller.setOnHeightChangedRunnable { viewModel.notificationStackChanged() }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
similarity index 64%
rename from packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
index 071127c..9483f33 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
@@ -26,17 +26,18 @@
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimClipping
+import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape
+import com.android.systemui.statusbar.notification.stack.shared.model.ViewPosition
 import com.android.systemui.util.kotlin.FlowDumperImpl
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
 
 /** ViewModel which represents the state of the NSSL/Controller in the world of flexiglass */
 @SysUISingleton
-class NotificationStackAppearanceViewModel
+class NotificationScrollViewModel
 @Inject
 constructor(
     dumpManager: DumpManager,
@@ -80,16 +81,48 @@
             .dumpWhileCollecting("expandFraction")
 
     /** The bounds of the notification stack in the current scene. */
-    val shadeScrimClipping: Flow<ShadeScrimClipping> =
+    private val shadeScrimClipping: Flow<ShadeScrimClipping?> =
         combine(
                 stackAppearanceInteractor.shadeScrimBounds,
                 stackAppearanceInteractor.shadeScrimRounding,
-                ::ShadeScrimClipping
-            )
+            ) { bounds, rounding ->
+                bounds?.let { ShadeScrimClipping(it, rounding) }
+            }
             .dumpWhileCollecting("stackClipping")
 
+    fun shadeScrimShape(
+        cornerRadius: Flow<Int>,
+        viewPosition: Flow<ViewPosition>
+    ): Flow<ShadeScrimShape?> =
+        combine(shadeScrimClipping, cornerRadius, viewPosition) { clipping, radius, position ->
+                if (clipping == null) return@combine null
+                ShadeScrimShape(
+                    bounds = clipping.bounds - position,
+                    topRadius = radius.takeIf { clipping.rounding.isTopRounded } ?: 0,
+                    bottomRadius = radius.takeIf { clipping.rounding.isBottomRounded } ?: 0
+                )
+            }
+            .dumpWhileCollecting("shadeScrimShape")
+
     /** The y-coordinate in px of top of the contents of the notification stack. */
-    val stackTop: StateFlow<Float> = stackAppearanceInteractor.stackTop.dumpValue("stackTop")
+    val stackTop: Flow<Float> = stackAppearanceInteractor.stackTop.dumpValue("stackTop")
+    /** The y-coordinate in px of bottom of the contents of the notification stack. */
+    val stackBottom: Flow<Float> = stackAppearanceInteractor.stackBottom.dumpValue("stackBottom")
+    /**
+     * Whether the notification stack is scrolled to the top; i.e., it cannot be scrolled down any
+     * further.
+     */
+    val scrolledToTop: Flow<Boolean> =
+        stackAppearanceInteractor.scrolledToTop.dumpValue("scrolledToTop")
+    /** The y-coordinate in px of bottom of the contents of the HUN. */
+    val headsUpTop: Flow<Float> = stackAppearanceInteractor.headsUpTop.dumpValue("headsUpTop")
+
+    /** Receives the amount (px) that the stack should scroll due to internal expansion. */
+    val syntheticScrollConsumer: (Float) -> Unit = stackAppearanceInteractor::setSyntheticScroll
+    /** Receives the height of the contents of the notification stack. */
+    val stackHeightConsumer: (Float) -> Unit = stackAppearanceInteractor::setStackHeight
+    /** Receives the height of the heads up notification. */
+    val headsUpHeightConsumer: (Float) -> Unit = stackAppearanceInteractor::setHeadsUpHeight
 
     /** Whether the notification stack is scrollable or not. */
     val isScrollable: Flow<Boolean> =
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 477f139..b284179 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
@@ -18,6 +18,7 @@
 
 import com.android.systemui.common.shared.model.NotificationContainerBounds
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FeatureFlagsClassic
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
@@ -26,8 +27,10 @@
 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds
 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimRounding
+import com.android.systemui.util.kotlin.FlowDumperImpl
 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
@@ -37,67 +40,72 @@
 class NotificationsPlaceholderViewModel
 @Inject
 constructor(
+    dumpManager: DumpManager,
     private val interactor: NotificationStackAppearanceInteractor,
     shadeInteractor: ShadeInteractor,
     flags: SceneContainerFlags,
     featureFlags: FeatureFlagsClassic,
     private val keyguardInteractor: KeyguardInteractor,
-) {
+) : FlowDumperImpl(dumpManager) {
     /** DEBUG: whether the placeholder should be made slightly visible for positional debugging. */
     val isVisualDebuggingEnabled: Boolean = featureFlags.isEnabled(Flags.NSSL_DEBUG_LINES)
 
     /** DEBUG: whether the debug logging should be output. */
     val isDebugLoggingEnabled: Boolean = flags.isEnabled()
 
-    /**
-     * Notifies that the bounds of the notification placeholder have changed.
-     *
-     * @param top The position of the top of the container in its window coordinate system, in
-     *   pixels.
-     * @param bottom The position of the bottom of the container in its window coordinate system, in
-     *   pixels.
-     */
-    fun onBoundsChanged(
-        left: Float,
+    /** Notifies that the bounds of the notification scrim have changed. */
+    fun onScrimBoundsChanged(bounds: ShadeScrimBounds?) {
+        interactor.setShadeScrimBounds(bounds)
+    }
+
+    /** Notifies that the bounds of the notification placeholder have changed. */
+    fun onStackBoundsChanged(
         top: Float,
-        right: Float,
         bottom: Float,
     ) {
         keyguardInteractor.setNotificationContainerBounds(
             NotificationContainerBounds(top = top, bottom = bottom)
         )
-        interactor.setShadeScrimBounds(
-            ShadeScrimBounds(top = top, bottom = bottom, left = left, right = right)
-        )
+        interactor.setStackTop(top)
+        interactor.setStackBottom(bottom)
+    }
+
+    /** Sets the available space */
+    fun onConstrainedAvailableSpaceChanged(height: Int) {
+        interactor.setConstrainedAvailableSpace(height)
+    }
+
+    fun onHeadsUpTopChanged(headsUpTop: Float) {
+        interactor.setHeadsUpTop(headsUpTop)
     }
 
     /** Corner rounding of the stack */
-    val shadeScrimRounding: Flow<ShadeScrimRounding> = interactor.shadeScrimRounding
+    val shadeScrimRounding: Flow<ShadeScrimRounding> =
+        interactor.shadeScrimRounding.dumpWhileCollecting("shadeScrimRounding")
 
     /**
      * 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
      * point the notification stack should become scrollable.
      */
-    val stackHeight = interactor.stackHeight
+    val stackHeight: StateFlow<Float> = interactor.stackHeight.dumpValue("stackHeight")
+
+    /** The height in px of the contents of the HUN. */
+    val headsUpHeight: StateFlow<Float> = interactor.headsUpHeight.dumpValue("headsUpHeight")
 
     /**
      * The amount [0-1] that the shade has been opened. At 0, the shade is closed; at 1, the shade
      * is open.
      */
-    val expandFraction: Flow<Float> = shadeInteractor.shadeExpansion
+    val expandFraction: Flow<Float> = shadeInteractor.shadeExpansion.dumpValue("expandFraction")
 
     /**
      * The amount in px that the notification stack should scroll due to internal expansion. This
      * should only happen when a notification expansion hits the bottom of the screen, so it is
      * necessary to scroll up to keep expanding the notification.
      */
-    val syntheticScroll: Flow<Float> = interactor.syntheticScroll
-
-    /** Sets the y-coord in px of the top of the contents of the notification stack. */
-    fun onContentTopChanged(padding: Float) {
-        interactor.setStackTop(padding)
-    }
+    val syntheticScroll: Flow<Float> =
+        interactor.syntheticScroll.dumpWhileCollecting("syntheticScroll")
 
     /** Sets whether the notification stack is scrolled to the top. */
     fun setScrolledToTop(scrolledToTop: Boolean) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index 9f57606..692368d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -60,7 +60,9 @@
 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
 import com.android.systemui.util.kotlin.BooleanFlowOperators.and
 import com.android.systemui.util.kotlin.BooleanFlowOperators.or
@@ -97,6 +99,7 @@
     private val keyguardInteractor: KeyguardInteractor,
     private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
     private val shadeInteractor: ShadeInteractor,
+    private val notificationStackAppearanceInteractor: NotificationStackAppearanceInteractor,
     private val alternateBouncerToGoneTransitionViewModel:
         AlternateBouncerToGoneTransitionViewModel,
     private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
@@ -364,6 +367,10 @@
                 initialValue = NotificationContainerBounds(),
             )
             .dumpValue("bounds")
+        get() {
+            /* check if */ SceneContainerFlag.isUnexpectedlyInLegacyMode()
+            return field
+        }
 
     /**
      * Ensure view is visible when the shade/qs are expanded. Also, as QS is expanding, fade out
@@ -571,8 +578,11 @@
             .dumpWhileCollecting("translationX")
 
     private val availableHeight: Flow<Float> =
-        bounds
-            .map { it.bottom - it.top }
+        if (SceneContainerFlag.isEnabled) {
+                notificationStackAppearanceInteractor.constrainedAvailableSpace.map { it.toFloat() }
+            } else {
+                bounds.map { it.bottom - it.top }
+            }
             .distinctUntilChanged()
             .dumpWhileCollecting("availableHeight")
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 2798dbf..5baf6a0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -28,7 +28,6 @@
 import static com.android.systemui.Dependency.TIME_TICK_HANDLER_NAME;
 import static com.android.systemui.Flags.lightRevealMigration;
 import static com.android.systemui.Flags.newAodTransition;
-import static com.android.systemui.Flags.predictiveBackSysui;
 import static com.android.systemui.Flags.truncatedStatusBarIconsFix;
 import static com.android.systemui.charging.WirelessChargingAnimation.UNKNOWN_BATTERY_LEVEL;
 import static com.android.systemui.statusbar.NotificationLockscreenUserManager.PERMISSION_SELF;
@@ -837,7 +836,7 @@
         mLightRevealScrimViewModelLazy = lightRevealScrimViewModelLazy;
         mLightRevealScrim = lightRevealScrim;
 
-        if (predictiveBackSysui()) {
+        if (PredictiveBackSysUiFlag.isEnabled()) {
             mContext.getApplicationInfo().setEnableOnBackInvokedCallback(true);
         }
     }
@@ -3061,7 +3060,7 @@
         public void onConfigChanged(Configuration newConfig) {
             updateResources();
             updateDisplaySize(); // populates mDisplayMetrics
-            if (predictiveBackSysui()) {
+            if (PredictiveBackSysUiFlag.isEnabled()) {
                 mContext.getApplicationInfo().setEnableOnBackInvokedCallback(true);
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PredictiveBackSysUiFlag.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PredictiveBackSysUiFlag.kt
new file mode 100644
index 0000000..74d6ba5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PredictiveBackSysUiFlag.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the predictive back flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object PredictiveBackSysUiFlag {
+    /** The aconfig flag name */
+    const val FLAG_NAME = Flags.FLAG_PREDICTIVE_BACK_SYSUI
+
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
+    /** Is the refactor enabled */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.predictiveBackSysui()
+
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun isUnexpectedlyInLegacyMode() =
+        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
index 20a82a4..d99af2d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
@@ -165,6 +165,8 @@
     public void showNotification(@NonNull NotificationEntry entry) {
         HeadsUpEntry headsUpEntry = createHeadsUpEntry(entry);
 
+        mLogger.logShowNotificationRequest(entry);
+
         Runnable runnable = () -> {
             // TODO(b/315362456) log outside runnable too
             mLogger.logShowNotification(entry);
@@ -219,6 +221,8 @@
      */
     public void updateNotification(@NonNull String key, boolean shouldHeadsUpAgain) {
         HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key);
+        mLogger.logUpdateNotificationRequest(key, shouldHeadsUpAgain, headsUpEntry != null);
+
         Runnable runnable = () -> {
             updateNotificationInternal(key, shouldHeadsUpAgain);
         };
@@ -378,8 +382,11 @@
      */
     protected final void removeEntry(@NonNull String key) {
         HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key);
+        mLogger.logRemoveEntryRequest(key);
 
         Runnable runnable = () -> {
+            mLogger.logRemoveEntry(key);
+
             if (headsUpEntry == null) {
                 return;
             }
@@ -566,8 +573,10 @@
     public void unpinAll(boolean userUnPinned) {
         for (String key : mHeadsUpEntryMap.keySet()) {
             HeadsUpEntry headsUpEntry = getHeadsUpEntry(key);
-
+            mLogger.logUnpinEntryRequest(key);
             Runnable runnable = () -> {
+                mLogger.logUnpinEntry(key);
+
                 setEntryPinned(headsUpEntry, false /* isPinned */);
                 // maybe it got un sticky
                 headsUpEntry.updateEntry(false /* updatePostTime */, "unpinAll");
@@ -886,6 +895,7 @@
          * Clear any pending removal runnables.
          */
         public void cancelAutoRemovalCallbacks(@Nullable String reason) {
+            mLogger.logAutoRemoveCancelRequest(this.mEntry, reason);
             Runnable runnable = () -> {
                 final boolean removed = cancelAutoRemovalCallbackInternal();
 
@@ -900,6 +910,7 @@
         public void scheduleAutoRemovalCallback(FinishTimeUpdater finishTimeCalculator,
                 @NonNull String reason) {
 
+            mLogger.logAutoRemoveRequest(this.mEntry, reason);
             Runnable runnable = () -> {
                 long delayMs = finishTimeCalculator.updateAndGetTimeRemaining();
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
index f6154afe..a306606 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
@@ -58,6 +58,14 @@
         })
     }
 
+    fun logShowNotificationRequest(entry: NotificationEntry) {
+        buffer.log(TAG, INFO, {
+            str1 = entry.logKey
+        }, {
+            "request: show notification $str1"
+        })
+    }
+
     fun logShowNotification(entry: NotificationEntry) {
         buffer.log(TAG, INFO, {
             str1 = entry.logKey
@@ -76,6 +84,15 @@
         })
     }
 
+    fun logAutoRemoveRequest(entry: NotificationEntry, reason: String) {
+        buffer.log(TAG, INFO, {
+            str1 = entry.logKey
+            str2 = reason
+        }, {
+            "request: reschedule auto remove of $str1 reason: $str2"
+        })
+    }
+
     fun logAutoRemoveRescheduled(entry: NotificationEntry, delayMillis: Long, reason: String) {
         buffer.log(TAG, INFO, {
             str1 = entry.logKey
@@ -86,6 +103,15 @@
         })
     }
 
+    fun logAutoRemoveCancelRequest(entry: NotificationEntry, reason: String?) {
+        buffer.log(TAG, INFO, {
+            str1 = entry.logKey
+            str2 = reason ?: "unknown"
+        }, {
+            "request: cancel auto remove of $str1 reason: $str2"
+        })
+    }
+
     fun logAutoRemoveCanceled(entry: NotificationEntry, reason: String?) {
         buffer.log(TAG, INFO, {
             str1 = entry.logKey
@@ -95,6 +121,38 @@
         })
     }
 
+    fun logRemoveEntryRequest(key: String) {
+        buffer.log(TAG, INFO, {
+            str1 = logKey(key)
+        }, {
+            "request: remove entry $str1"
+        })
+    }
+
+    fun logRemoveEntry(key: String) {
+        buffer.log(TAG, INFO, {
+            str1 = logKey(key)
+        }, {
+            "remove entry $str1"
+        })
+    }
+
+    fun logUnpinEntryRequest(key: String) {
+        buffer.log(TAG, INFO, {
+            str1 = logKey(key)
+        }, {
+            "request: unpin entry $str1"
+        })
+    }
+
+    fun logUnpinEntry(key: String) {
+        buffer.log(TAG, INFO, {
+            str1 = logKey(key)
+        }, {
+            "unpin entry $str1"
+        })
+    }
+
     fun logRemoveNotification(key: String, releaseImmediately: Boolean) {
         buffer.log(TAG, INFO, {
             str1 = logKey(key)
@@ -112,6 +170,16 @@
         })
     }
 
+    fun logUpdateNotificationRequest(key: String, alert: Boolean, hasEntry: Boolean) {
+        buffer.log(TAG, INFO, {
+            str1 = logKey(key)
+            bool1 = alert
+            bool2 = hasEntry
+        }, {
+            "request: update notification $str1 alert: $bool1 hasEntry: $bool2 reason: $str2"
+        })
+    }
+
     fun logUpdateNotification(key: String, alert: Boolean, hasEntry: Boolean) {
         buffer.log(TAG, INFO, {
             str1 = logKey(key)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
index 860068c..0d53277 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
@@ -20,6 +20,7 @@
 import static android.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS;
 
 import static com.android.server.notification.Flags.screenshareNotificationHiding;
+import static com.android.systemui.Flags.screenshareNotificationHidingBugFix;
 
 import android.annotation.MainThread;
 import android.app.IActivityManager;
@@ -31,6 +32,7 @@
 import android.os.Handler;
 import android.os.RemoteException;
 import android.os.Trace;
+import android.os.UserHandle;
 import android.service.notification.StatusBarNotification;
 import android.util.ArraySet;
 import android.util.Log;
@@ -316,6 +318,10 @@
             return false;
         }
 
+        if (screenshareNotificationHidingBugFix() && UserHandle.isCore(sbn.getUid())) {
+            return false; // do not hide/redact notifications from system uid
+        }
+
         // Only protect/redact notifications if the developer has not explicitly set notification
         // visibility as public and users has not adjusted default channel visibility to private
         boolean notificationRequestsRedaction = entry.isNotificationVisibilityPrivate();
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandleExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandleExt.kt
index 909a18be..97d957d 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandleExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandleExt.kt
@@ -17,8 +17,14 @@
 package com.android.systemui.util.kotlin
 
 import com.android.systemui.lifecycle.repeatWhenAttached
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
 import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.launch
 
 /**
  * Suspends to keep getting updates until cancellation. Once cancelled, mark this as eligible for
@@ -42,3 +48,22 @@
         dispose()
     }
 }
+
+/**
+ * This will [launch], run [onLaunch] to get a [DisposableHandle], and finally
+ * [awaitCancellationThenDispose][DisposableHandle.awaitCancellationThenDispose]. This can be used
+ * to structure self-disposing code which attaches listeners, for example in ViewBinders:
+ * ```
+ * suspend fun bind(view: MyView, viewModel: MyViewModel) = coroutineScope {
+ *     launchAndDispose {
+ *         view.setOnClickListener { viewModel.handleClick() }
+ *         DisposableHandle { view.setOnClickListener(null) }
+ *     }
+ * }
+ * ```
+ */
+inline fun CoroutineScope.launchAndDispose(
+    context: CoroutineContext = EmptyCoroutineContext,
+    start: CoroutineStart = CoroutineStart.DEFAULT,
+    crossinline onLaunch: () -> DisposableHandle
+): Job = launch(context, start) { onLaunch().awaitCancellationThenDispose() }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
index 2ca5aef..c47f0bc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
@@ -23,6 +23,7 @@
 import android.view.View.VISIBLE
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.test.filters.SmallTest
+import com.android.internal.policy.SystemBarUtils
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
@@ -70,7 +71,7 @@
             Utils.getStatusBarHeaderHeightKeyguard(context)
 
     private val LARGE_CLOCK_TOP_WITHOUT_SMARTSPACE =
-        context.resources.getDimensionPixelSize(R.dimen.status_bar_height) +
+        SystemBarUtils.getStatusBarHeight(context) +
             context.resources.getDimensionPixelSize(
                 com.android.systemui.customization.R.dimen.small_clock_padding_top
             ) +
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt
deleted file mode 100644
index e044eec..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.mediaprojection.permission
-
-import android.app.AlertDialog
-import android.media.projection.MediaProjectionConfig
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import android.view.WindowManager
-import android.widget.Spinner
-import android.widget.TextView
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.FeatureFlagsClassic
-import com.android.systemui.flags.Flags
-import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
-import com.android.systemui.res.R
-import com.android.systemui.statusbar.phone.AlertDialogWithDelegate
-import com.android.systemui.statusbar.phone.SystemUIDialog
-import com.android.systemui.util.mockito.mock
-import junit.framework.Assert.assertEquals
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mockito.`when` as whenever
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-class MediaProjectionPermissionDialogDelegateTest : SysuiTestCase() {
-
-    private lateinit var dialog: AlertDialog
-
-    private val flags = mock<FeatureFlagsClassic>()
-    private val onStartRecordingClicked = mock<Runnable>()
-    private val mediaProjectionMetricsLogger = mock<MediaProjectionMetricsLogger>()
-
-    private val mediaProjectionConfig: MediaProjectionConfig =
-        MediaProjectionConfig.createConfigForDefaultDisplay()
-    private val appName: String = "testApp"
-    private val hostUid: Int = 12345
-
-    private val resIdSingleApp = R.string.screen_share_permission_dialog_option_single_app
-    private val resIdFullScreen = R.string.screen_share_permission_dialog_option_entire_screen
-    private val resIdSingleAppDisabled =
-        R.string.media_projection_entry_app_permission_dialog_single_app_disabled
-
-    @Before
-    fun setUp() {
-        whenever(flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)).thenReturn(true)
-    }
-
-    @After
-    fun teardown() {
-        if (::dialog.isInitialized) {
-            dialog.dismiss()
-        }
-    }
-
-    @Test
-    fun showDialog_forceShowPartialScreenShareFalse() {
-        // Set up dialog with MediaProjectionConfig.createConfigForDefaultDisplay() and
-        // overrideDisableSingleAppOption = false
-        val overrideDisableSingleAppOption = false
-        setUpAndShowDialog(overrideDisableSingleAppOption)
-
-        val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_spinner)
-        val secondOptionText =
-            spinner.adapter
-                .getDropDownView(1, null, spinner)
-                .findViewById<TextView>(android.R.id.text2)
-                ?.text
-
-        // check that the first option is full screen and enabled
-        assertEquals(context.getString(resIdFullScreen), spinner.selectedItem)
-
-        // check that the second option is single app and disabled
-        assertEquals(context.getString(resIdSingleAppDisabled, appName), secondOptionText)
-    }
-
-    @Test
-    fun showDialog_forceShowPartialScreenShareTrue() {
-        // Set up dialog with MediaProjectionConfig.createConfigForDefaultDisplay() and
-        // overrideDisableSingleAppOption = true
-        val overrideDisableSingleAppOption = true
-        setUpAndShowDialog(overrideDisableSingleAppOption)
-
-        val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_spinner)
-        val secondOptionText =
-            spinner.adapter
-                .getDropDownView(1, null, spinner)
-                .findViewById<TextView>(android.R.id.text1)
-                ?.text
-
-        // check that the first option is single app and enabled
-        assertEquals(context.getString(resIdSingleApp), spinner.selectedItem)
-
-        // check that the second option is full screen and enabled
-        assertEquals(context.getString(resIdFullScreen), secondOptionText)
-    }
-
-    private fun setUpAndShowDialog(overrideDisableSingleAppOption: Boolean) {
-        val delegate =
-            MediaProjectionPermissionDialogDelegate(
-                context,
-                mediaProjectionConfig,
-                {},
-                onStartRecordingClicked,
-                appName,
-                overrideDisableSingleAppOption,
-                hostUid,
-                mediaProjectionMetricsLogger
-            )
-
-        dialog = AlertDialogWithDelegate(context, R.style.Theme_SystemUI_Dialog, delegate)
-        SystemUIDialog.applyFlags(dialog)
-        SystemUIDialog.setDialogSize(dialog)
-
-        dialog.window?.addSystemFlags(
-            WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS
-        )
-
-        delegate.onCreate(dialog, savedInstanceState = null)
-        dialog.show()
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index 02f2e16..cf7c6f4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -20,6 +20,7 @@
 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
 import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
 import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
+import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -436,6 +437,10 @@
 
         verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture());
         assertThat((mLayoutParameters.getValue().flags & FLAG_SECURE) != 0).isTrue();
+        assertThat(
+                (mLayoutParameters.getValue().inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_TRACING)
+                        != 0)
+                .isTrue();
     }
 
     @Test
@@ -444,6 +449,10 @@
 
         verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture());
         assertThat((mLayoutParameters.getValue().flags & FLAG_SECURE) == 0).isTrue();
+        assertThat(
+                (mLayoutParameters.getValue().inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_TRACING)
+                        == 0)
+                .isTrue();
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index cd8be57..912ecb3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -94,7 +94,6 @@
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController.NotificationPanelEvent;
 import com.android.systemui.statusbar.notification.stack.NotificationSwipeHelper.NotificationCallback;
-import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor;
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -161,7 +160,6 @@
     @Mock private VisibilityLocationProviderDelegator mVisibilityLocationProviderDelegator;
     @Mock private ShadeController mShadeController;
     @Mock private Provider<WindowRootView> mWindowRootView;
-    @Mock private NotificationStackAppearanceInteractor mNotificationStackAppearanceInteractor;
     private final StackStateLogger mStackLogger = new StackStateLogger(logcatLogBuffer(),
             logcatLogBuffer());
     private final NotificationStackScrollLogger mLogger = new NotificationStackScrollLogger(
@@ -1016,7 +1014,6 @@
                 mViewBinder,
                 mShadeController,
                 mWindowRootView,
-                mNotificationStackAppearanceInteractor,
                 mKosmos.getInteractionJankMonitor(),
                 mStackLogger,
                 mLogger,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
index 581ca3b..4ace163 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
@@ -28,8 +28,10 @@
 import android.content.pm.PackageManager
 import android.media.projection.MediaProjectionInfo
 import android.media.projection.MediaProjectionManager
+import android.os.Process
 import android.os.UserHandle
 import android.permission.flags.Flags.FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION
+import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.platform.test.annotations.RequiresFlagsDisabled
 import android.platform.test.annotations.RequiresFlagsEnabled
@@ -41,7 +43,8 @@
 import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
 import com.android.dx.mockito.inline.extended.ExtendedMockito.verify
 import com.android.internal.util.FrameworkStatsLog
-import com.android.server.notification.Flags
+import com.android.server.notification.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING
+import com.android.systemui.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.statusbar.RankingBuilder
@@ -77,7 +80,7 @@
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
 @RunWithLooper
-@EnableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+@EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
 class SensitiveNotificationProtectionControllerTest : SysuiTestCase() {
     @get:Rule val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
 
@@ -384,13 +387,33 @@
     }
 
     @Test
+    @DisableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX)
+    fun shouldProtectNotification_projectionActive_isFromCoreApp_fixDisabled_true() {
+        mediaProjectionCallback.onStart(mediaProjectionInfo)
+
+        val notificationEntry = setupCoreAppNotificationEntry(TEST_PROJECTION_PACKAGE_NAME)
+
+        assertTrue(controller.shouldProtectNotification(notificationEntry))
+    }
+
+    @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX)
+    fun shouldProtectNotification_projectionActive_isFromCoreApp_false() {
+        mediaProjectionCallback.onStart(mediaProjectionInfo)
+
+        val notificationEntry = setupCoreAppNotificationEntry(TEST_PROJECTION_PACKAGE_NAME)
+
+        assertFalse(controller.shouldProtectNotification(notificationEntry))
+    }
+
+    @Test
     fun shouldProtectNotification_projectionActive_sysuiExempt_false() {
         // SystemUi context package name is exempt, but in test scenarios its
         // com.android.systemui.tests so use that instead of hardcoding
         setShareFullScreenViaSystemUi()
         mediaProjectionCallback.onStart(mediaProjectionInfo)
 
-        val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME, false)
+        val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME)
 
         assertFalse(controller.shouldProtectNotification(notificationEntry))
     }
@@ -407,7 +430,7 @@
             .thenReturn(PackageManager.PERMISSION_GRANTED)
         mediaProjectionCallback.onStart(mediaProjectionInfo)
 
-        val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME, false)
+        val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME)
 
         assertTrue(controller.shouldProtectNotification(notificationEntry))
     }
@@ -424,7 +447,7 @@
             .thenReturn(PackageManager.PERMISSION_GRANTED)
         mediaProjectionCallback.onStart(mediaProjectionInfo)
 
-        val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME, false)
+        val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME)
 
         assertFalse(controller.shouldProtectNotification(notificationEntry))
     }
@@ -434,7 +457,7 @@
         setShareFullScreenViaBugReportHandler()
         mediaProjectionCallback.onStart(mediaProjectionInfo)
 
-        val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME, false)
+        val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME)
 
         assertFalse(controller.shouldProtectNotification(notificationEntry))
     }
@@ -657,6 +680,7 @@
     private fun setupNotificationEntry(
         packageName: String,
         isFgs: Boolean = false,
+        isCoreApp: Boolean = false,
         overrideVisibility: Boolean = false,
         overrideChannelVisibility: Boolean = false,
     ): NotificationEntry {
@@ -668,8 +692,14 @@
             // Developer has marked notification as public
             notification.visibility = VISIBILITY_PUBLIC
         }
-        val notificationEntry =
-            NotificationEntryBuilder().setNotification(notification).setPkg(packageName).build()
+        val notificationEntryBuilder =
+            NotificationEntryBuilder().setNotification(notification).setPkg(packageName)
+        if (isCoreApp) {
+            notificationEntryBuilder.setUid(Process.FIRST_APPLICATION_UID - 10)
+        } else {
+            notificationEntryBuilder.setUid(Process.FIRST_APPLICATION_UID + 10)
+        }
+        val notificationEntry = notificationEntryBuilder.build()
         val channel = NotificationChannel("1", "1", IMPORTANCE_HIGH)
         if (overrideChannelVisibility) {
             // User doesn't allow private notifications at the channel level
@@ -688,6 +718,10 @@
         return setupNotificationEntry(packageName, isFgs = true)
     }
 
+    private fun setupCoreAppNotificationEntry(packageName: String): NotificationEntry {
+        return setupNotificationEntry(packageName, isCoreApp = true)
+    }
+
     private fun setupPublicNotificationEntry(packageName: String): NotificationEntry {
         return setupNotificationEntry(packageName, overrideVisibility = true)
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt
index f65c74f..5934e04 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR
 import com.android.systemui.Flags.FLAG_MEDIA_IN_SCENE_CONTAINER
 import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT
+import com.android.systemui.Flags.FLAG_PREDICTIVE_BACK_SYSUI
 import com.android.systemui.Flags.FLAG_SCENE_CONTAINER
 
 /**
@@ -35,6 +36,7 @@
     FLAG_COMPOSE_LOCKSCREEN,
     FLAG_MEDIA_IN_SCENE_CONTAINER,
     FLAG_KEYGUARD_WM_STATE_REFACTOR,
+    FLAG_PREDICTIVE_BACK_SYSUI,
 )
 @Retention(AnnotationRetention.RUNTIME)
 @Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorKosmos.kt
index d791e94..12165cd 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorKosmos.kt
@@ -20,4 +20,4 @@
 import com.android.systemui.kosmos.Kosmos
 
 val Kosmos.keyguardClockInteractor by
-    Kosmos.Fixture { KeyguardClockInteractor(keyguardClockRepository = keyguardClockRepository) }
+    Kosmos.Fixture { KeyguardClockInteractor(keyguardClockRepository) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt
index 4d902fa..df08e4a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt
@@ -60,4 +60,8 @@
     override suspend fun applyBottomNavBarPadding(padding: Int) {
         _navBarPadding.value = padding
     }
+
+    override fun requestCloseCustomizer() {
+        _customizing.value = false
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt
index 4a2eaf0..d08855f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.assist.AssistManager
 import com.android.systemui.concurrency.fakeExecutor
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testDispatcher
@@ -58,6 +59,7 @@
             statusBarKeyguardViewManager = mock<StatusBarKeyguardViewManager>(),
             notificationShadeWindowController = mock<NotificationShadeWindowController>(),
             assistManagerLazy = { mock<AssistManager>() },
+            deviceUnlockedInteractor = deviceUnlockedInteractor,
         )
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModelKosmos.kt
similarity index 92%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModelKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModelKosmos.kt
index bada2a6..10cc136 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModelKosmos.kt
@@ -23,8 +23,8 @@
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor
 
-val Kosmos.notificationStackAppearanceViewModel by Fixture {
-    NotificationStackAppearanceViewModel(
+val Kosmos.notificationScrollViewModel by Fixture {
+    NotificationScrollViewModel(
         dumpManager = dumpManager,
         stackAppearanceInteractor = notificationStackAppearanceInteractor,
         shadeInteractor = shadeInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt
index 29faa58..b249211 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.notification.stack.ui.viewmodel
 
+import com.android.systemui.dump.dumpManager
 import com.android.systemui.flags.featureFlagsClassic
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.kosmos.Kosmos
@@ -26,6 +27,7 @@
 
 val Kosmos.notificationsPlaceholderViewModel by Fixture {
     NotificationsPlaceholderViewModel(
+        dumpManager = dumpManager,
         interactor = notificationStackAppearanceInteractor,
         shadeInteractor = shadeInteractor,
         flags = sceneContainerFlags,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
index de0cc65..d2de835 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
@@ -43,6 +43,7 @@
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 
@@ -55,6 +56,7 @@
         keyguardInteractor = keyguardInteractor,
         keyguardTransitionInteractor = keyguardTransitionInteractor,
         shadeInteractor = shadeInteractor,
+        notificationStackAppearanceInteractor = notificationStackAppearanceInteractor,
         alternateBouncerToGoneTransitionViewModel = alternateBouncerToGoneTransitionViewModel,
         aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel,
         aodToOccludedTransitionViewModel = aodToOccludedTransitionViewModel,
diff --git a/services/accessibility/Android.bp b/services/accessibility/Android.bp
index 467adc7..7a99b60 100644
--- a/services/accessibility/Android.bp
+++ b/services/accessibility/Android.bp
@@ -58,6 +58,7 @@
 aconfig_declarations {
     name: "com_android_server_accessibility_flags",
     package: "com.android.server.accessibility",
+    container: "system",
     srcs: [
         "accessibility.aconfig",
     ],
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index 04b19ff..1d6399e 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.accessibility"
+container: "system"
 
 # NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors.
 
diff --git a/services/backup/Android.bp b/services/backup/Android.bp
index 2a85eb6..e13746e 100644
--- a/services/backup/Android.bp
+++ b/services/backup/Android.bp
@@ -30,5 +30,6 @@
 aconfig_declarations {
     name: "backup_flags",
     package: "com.android.server.backup",
+    container: "system",
     srcs: ["flags.aconfig"],
 }
diff --git a/services/backup/flags.aconfig b/services/backup/flags.aconfig
index 74adfe0..d53f949 100644
--- a/services/backup/flags.aconfig
+++ b/services/backup/flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.backup"
+container: "system"
 
 flag {
     name: "enable_skipping_restore_launched_apps"
@@ -58,4 +59,4 @@
     description: "Increase BMM logging coverage in restore at install flow."
     bug: "331749778"
     is_fixed_read_only: true
-}
\ No newline at end of file
+}
diff --git a/services/companion/java/com/android/server/companion/virtual/Android.bp b/services/companion/java/com/android/server/companion/virtual/Android.bp
index 4a2030f..66313e6 100644
--- a/services/companion/java/com/android/server/companion/virtual/Android.bp
+++ b/services/companion/java/com/android/server/companion/virtual/Android.bp
@@ -10,6 +10,7 @@
 aconfig_declarations {
     name: "virtualdevice_flags",
     package: "com.android.server.companion.virtual",
+    container: "system",
     srcs: [
         "flags.aconfig",
     ],
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index da90bdb..06431fc 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -1068,6 +1068,10 @@
 
     @Override
     public boolean hasCustomAudioInputSupport() throws RemoteException {
+        return hasCustomAudioInputSupportInternal();
+    }
+
+    private boolean hasCustomAudioInputSupportInternal() {
         if (!Flags.vdmPublicApis()) {
             return false;
         }
@@ -1108,10 +1112,10 @@
         mParams.dump(fout, indent + indent);
         fout.println(indent + "mVirtualDisplayIds: ");
         synchronized (mVirtualDeviceLock) {
-            fout.println("    mDevicePolicies: " + mDevicePolicies);
             for (int i = 0; i < mVirtualDisplays.size(); i++) {
                 fout.println(indent + "  " + mVirtualDisplays.keyAt(i));
             }
+            fout.println("    mDevicePolicies: " + mDevicePolicies);
             fout.println(indent + "mDefaultShowPointerIcon: " + mDefaultShowPointerIcon);
         }
         mInputController.dump(fout);
@@ -1119,6 +1123,8 @@
         if (mVirtualCameraController != null) {
             mVirtualCameraController.dump(fout, indent);
         }
+        fout.println(
+                indent + "hasCustomAudioInputSupport: " + hasCustomAudioInputSupportInternal());
     }
 
     // For display mirroring, we want to dispatch all key events to the source (default) display,
diff --git a/services/companion/java/com/android/server/companion/virtual/flags.aconfig b/services/companion/java/com/android/server/companion/virtual/flags.aconfig
index 6297e91..616f5d0 100644
--- a/services/companion/java/com/android/server/companion/virtual/flags.aconfig
+++ b/services/companion/java/com/android/server/companion/virtual/flags.aconfig
@@ -1,6 +1,7 @@
 # OLD PACKAGE, DO NOT USE: Prefer `flags.aconfig` in core/java/android/companion/virtual
 # (or other custom files) to define your flags
 package: "com.android.server.companion.virtual"
+container: "system"
 
 flag {
   name: "dump_history"
diff --git a/services/core/java/com/android/server/TEST_MAPPING b/services/core/java/com/android/server/TEST_MAPPING
index 9c4c634..baae40b 100644
--- a/services/core/java/com/android/server/TEST_MAPPING
+++ b/services/core/java/com/android/server/TEST_MAPPING
@@ -62,6 +62,18 @@
             "file_patterns": ["SensorPrivacyService\\.java"]
         },
         {
+            "name": "FrameworksMockingServicesTests",
+            "options": [
+                {
+                    "include-filter": "com.android.server.SensitiveContentProtectionManagerServiceContentTest"
+                },
+                {
+                    "include-filter": "com.android.server.SensitiveContentProtectionManagerServiceNotificationTest"
+                }
+            ],
+            "file_patterns": ["SensitiveContentProtectionManagerService\\.java"]
+        },
+        {
             "name": "FrameworksServicesTests",
             "options": [
                 {
diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java
index 586b095..603a95c 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerService.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java
@@ -897,6 +897,11 @@
                         }
                     }
                     for (String packageNameToNotify : accountRemovedReceivers) {
+                        int currentVisibility =
+                                resolveAccountVisibility(account, packageNameToNotify, accounts);
+                        if (isVisible(currentVisibility)) {
+                            continue;
+                        }
                         sendAccountRemovedBroadcast(
                                 account,
                                 packageNameToNotify,
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 0a2aaeb..7ea82b0 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -5555,6 +5555,7 @@
             boolean enqueueOomAdj, @ServiceBindingOomAdjPolicy int serviceBindingOomAdjPolicy)
             throws TransactionTooLargeException {
         if (r.app != null && r.app.isThreadReady()) {
+            r.updateOomAdjSeq();
             sendServiceArgsLocked(r, execInFg, false);
             return null;
         }
diff --git a/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java b/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java
index 55b161a..dcda5c2 100644
--- a/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java
+++ b/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java
@@ -55,9 +55,7 @@
     static final boolean DEBUG_BACKGROUND_CHECK = DEBUG_ALL || false;
     static final boolean DEBUG_BACKUP = DEBUG_ALL || false;
     static final boolean DEBUG_BROADCAST = DEBUG_ALL || false;
-    static final boolean DEBUG_BROADCAST_BACKGROUND = DEBUG_BROADCAST || false;
     static final boolean DEBUG_BROADCAST_LIGHT = DEBUG_BROADCAST || false;
-    static final boolean DEBUG_BROADCAST_DEFERRAL = DEBUG_BROADCAST || false;
     static final boolean DEBUG_COMPACTION = DEBUG_ALL || false;
     static final boolean DEBUG_FREEZER = DEBUG_ALL || false;
     static final boolean DEBUG_LRU = DEBUG_ALL || false;
diff --git a/services/core/java/com/android/server/am/Android.bp b/services/core/java/com/android/server/am/Android.bp
index af1200e..0294ffe 100644
--- a/services/core/java/com/android/server/am/Android.bp
+++ b/services/core/java/com/android/server/am/Android.bp
@@ -1,6 +1,7 @@
 aconfig_declarations {
     name: "am_flags",
     package: "com.android.server.am",
+    container: "system",
     srcs: ["*.aconfig"],
 }
 
diff --git a/services/core/java/com/android/server/am/OWNERS b/services/core/java/com/android/server/am/OWNERS
index a656287..b517631 100644
--- a/services/core/java/com/android/server/am/OWNERS
+++ b/services/core/java/com/android/server/am/OWNERS
@@ -9,7 +9,6 @@
 sudheersai@google.com
 suprabh@google.com
 varunshah@google.com
-kwekua@google.com
 bookatz@google.com
 jji@google.com
 
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index 48a9d6a..6779f7a 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -351,6 +351,7 @@
     // LMK_UPDATE_PROPS
     // LMK_KILL_OCCURRED
     // LMK_START_MONITORING
+    // LMK_BOOT_COMPLETED
     static final byte LMK_TARGET = 0;
     static final byte LMK_PROCPRIO = 1;
     static final byte LMK_PROCREMOVE = 2;
@@ -361,6 +362,7 @@
     static final byte LMK_UPDATE_PROPS = 7;
     static final byte LMK_KILL_OCCURRED = 8; // Msg to subscribed clients on kill occurred event
     static final byte LMK_START_MONITORING = 9; // Start monitoring if delayed earlier
+    static final byte LMK_BOOT_COMPLETED = 10;
 
     // Low Memory Killer Daemon command codes.
     // These must be kept in sync with async_event_type definitions in lmkd.h
diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig
index fd847f1..e1ccf4d 100644
--- a/services/core/java/com/android/server/am/flags.aconfig
+++ b/services/core/java/com/android/server/am/flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.am"
+container: "system"
 
 flag {
     name: "oomadjuster_correctness_rewrite"
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 951f676..77654d4 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -1517,8 +1517,9 @@
         sendLMsgNoDelay(MSG_L_SYNCHRONIZE_ADI_DEVICES_IN_INVENTORY, SENDMSG_QUEUE, deviceState);
     }
 
-    /*package*/ void postUpdatedAdiDeviceState(AdiDeviceState deviceState) {
-        sendLMsgNoDelay(MSG_L_UPDATED_ADI_DEVICE_STATE, SENDMSG_QUEUE, deviceState);
+    /*package*/ void postUpdatedAdiDeviceState(AdiDeviceState deviceState, boolean initSA) {
+        sendILMsgNoDelay(
+                MSG_IL_UPDATED_ADI_DEVICE_STATE, SENDMSG_QUEUE, initSA ? 1 : 0, deviceState);
     }
 
     /*package*/ static final class CommunicationDeviceInfo {
@@ -1820,18 +1821,17 @@
                                         + "received with null profile proxy: "
                                         + btInfo)).printLog(TAG));
                     } else {
-                        @AudioSystem.AudioFormatNativeEnumForBtCodec final int codec =
+                        final Pair<Integer, Boolean> codecAndChanged =
                                 mBtHelper.getCodecWithFallback(btInfo.mDevice,
                                         btInfo.mProfile, btInfo.mIsLeOutput,
                                         "MSG_L_SET_BT_ACTIVE_DEVICE");
                         synchronized (mSetModeLock) {
                             synchronized (mDeviceStateLock) {
-                                mDeviceInventory.onSetBtActiveDevice(btInfo, codec,
-                                        (btInfo.mProfile
-                                                != BluetoothProfile.LE_AUDIO
+                                mDeviceInventory.onSetBtActiveDevice(btInfo, codecAndChanged.first,
+                                        (btInfo.mProfile != BluetoothProfile.LE_AUDIO
                                                 || btInfo.mIsLeOutput)
-                                                ? mAudioService.getBluetoothContextualVolumeStream()
-                                                : AudioSystem.STREAM_DEFAULT);
+                                            ? mAudioService.getBluetoothContextualVolumeStream()
+                                            : AudioSystem.STREAM_DEFAULT);
                                 if (btInfo.mProfile == BluetoothProfile.LE_AUDIO
                                         || btInfo.mProfile
                                         == BluetoothProfile.HEARING_AID) {
@@ -1866,13 +1866,13 @@
                     break;
                 case MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE: {
                     final BtDeviceInfo btInfo = (BtDeviceInfo) msg.obj;
-                    @AudioSystem.AudioFormatNativeEnumForBtCodec final int codec =
-                            mBtHelper.getCodecWithFallback(btInfo.mDevice,
-                                    btInfo.mProfile, btInfo.mIsLeOutput,
-                                    "MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE");
+                    final Pair<Integer, Boolean> codecAndChanged = mBtHelper.getCodecWithFallback(
+                            btInfo.mDevice, btInfo.mProfile, btInfo.mIsLeOutput,
+                            "MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE");
                     synchronized (mDeviceStateLock) {
-                        mDeviceInventory.onBluetoothDeviceConfigChange(
-                                btInfo, codec, BtHelper.EVENT_DEVICE_CONFIG_CHANGE);
+                        mDeviceInventory.onBluetoothDeviceConfigChange(btInfo,
+                                codecAndChanged.first, codecAndChanged.second,
+                                BtHelper.EVENT_DEVICE_CONFIG_CHANGE);
                     }
                 } break;
                 case MSG_BROADCAST_AUDIO_BECOMING_NOISY:
@@ -2049,8 +2049,8 @@
                         }
                     } break;
 
-                case MSG_L_UPDATED_ADI_DEVICE_STATE:
-                    mAudioService.onUpdatedAdiDeviceState((AdiDeviceState) msg.obj);
+                case MSG_IL_UPDATED_ADI_DEVICE_STATE:
+                    mAudioService.onUpdatedAdiDeviceState((AdiDeviceState) msg.obj, msg.arg1 == 1);
                     break;
 
                 default:
@@ -2137,7 +2137,7 @@
     private static final int MSG_CHECK_COMMUNICATION_ROUTE_CLIENT_STATE = 56;
     private static final int MSG_I_UPDATE_LE_AUDIO_GROUP_ADDRESSES = 57;
     private static final int MSG_L_SYNCHRONIZE_ADI_DEVICES_IN_INVENTORY = 58;
-    private static final int MSG_L_UPDATED_ADI_DEVICE_STATE = 59;
+    private static final int MSG_IL_UPDATED_ADI_DEVICE_STATE = 59;
 
 
 
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 14428c4..f38b381 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -173,7 +173,7 @@
                 if (ads.getAudioDeviceCategory() != category && (userDefined
                         || category != AUDIO_DEVICE_CATEGORY_UNKNOWN)) {
                     ads.setAudioDeviceCategory(category);
-                    mDeviceBroker.postUpdatedAdiDeviceState(ads);
+                    mDeviceBroker.postUpdatedAdiDeviceState(ads, false /*initSA*/);
                     mDeviceBroker.postPersistAudioDeviceSettings();
                 }
                 mDeviceBroker.postSynchronizeAdiDevicesInInventory(ads);
@@ -186,7 +186,7 @@
             mDeviceInventory.put(ads.getDeviceId(), ads);
             checkDeviceInventorySize_l();
 
-            mDeviceBroker.postUpdatedAdiDeviceState(ads);
+            mDeviceBroker.postUpdatedAdiDeviceState(ads, true /*initSA*/);
             mDeviceBroker.postPersistAudioDeviceSettings();
         }
     }
@@ -216,7 +216,7 @@
             checkDeviceInventorySize_l();
         }
         if (updatedCategory.get()) {
-            mDeviceBroker.postUpdatedAdiDeviceState(deviceState);
+            mDeviceBroker.postUpdatedAdiDeviceState(deviceState, false /*initSA*/);
         }
         mDeviceBroker.postSynchronizeAdiDevicesInInventory(deviceState);
     }
@@ -318,7 +318,7 @@
                     }
                     ads2.setAudioDeviceCategory(updatedDevice.getAudioDeviceCategory());
 
-                    mDeviceBroker.postUpdatedAdiDeviceState(ads2);
+                    mDeviceBroker.postUpdatedAdiDeviceState(ads2, false /*initSA*/);
                     AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                             "synchronizeBleDeviceInInventory synced device pair ads1="
                                     + updatedDevice + " ads2=" + ads2).printLog(TAG));
@@ -339,7 +339,7 @@
                     }
                     ads2.setAudioDeviceCategory(updatedDevice.getAudioDeviceCategory());
 
-                    mDeviceBroker.postUpdatedAdiDeviceState(ads2);
+                    mDeviceBroker.postUpdatedAdiDeviceState(ads2, false /*initSA*/);
                     AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                             "synchronizeBleDeviceInInventory synced device pair ads1="
                                     + updatedDevice + " peer ads2=" + ads2).printLog(TAG));
@@ -364,7 +364,7 @@
             }
             ads.setAudioDeviceCategory(updatedDevice.getAudioDeviceCategory());
 
-            mDeviceBroker.postUpdatedAdiDeviceState(ads);
+            mDeviceBroker.postUpdatedAdiDeviceState(ads, false /*initSA*/);
             AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                     "synchronizeDeviceProfilesInInventory synced device pair ads1="
                             + updatedDevice + " ads2=" + ads).printLog(TAG));
@@ -868,7 +868,8 @@
     @GuardedBy("mDeviceBroker.mDeviceStateLock")
     /*package*/ void onBluetoothDeviceConfigChange(
             @NonNull AudioDeviceBroker.BtDeviceInfo btInfo,
-            @AudioSystem.AudioFormatNativeEnumForBtCodec int codec, int event) {
+            @AudioSystem.AudioFormatNativeEnumForBtCodec int codec,
+            boolean codecChanged, int event) {
         MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId
                 + "onBluetoothDeviceConfigChange")
                 .set(MediaMetrics.Property.EVENT, BtHelper.deviceEventToString(event));
@@ -916,14 +917,12 @@
 
 
             if (event == BtHelper.EVENT_DEVICE_CONFIG_CHANGE) {
-                boolean codecChange = false;
                 if (btInfo.mProfile == BluetoothProfile.A2DP
                         || btInfo.mProfile == BluetoothProfile.LE_AUDIO
                         || btInfo.mProfile == BluetoothProfile.LE_AUDIO_BROADCAST) {
-                    if (di.mDeviceCodecFormat != codec) {
+                    if (codecChanged) {
                         di.mDeviceCodecFormat = codec;
                         mConnectedDevices.replace(key, di);
-                        codecChange = true;
                         final int res = mAudioSystem.handleDeviceConfigChange(
                                 btInfo.mAudioSystemDevice, address,
                                 BtHelper.getName(btDevice), codec);
@@ -947,7 +946,7 @@
                         }
                     }
                 }
-                if (!codecChange) {
+                if (!codecChanged) {
                     updateBluetoothPreferredModes_l(btDevice /*connectedDevice*/);
                 }
             }
diff --git a/services/core/java/com/android/server/audio/AudioManagerShellCommand.java b/services/core/java/com/android/server/audio/AudioManagerShellCommand.java
index 85acf70..570d4e9 100644
--- a/services/core/java/com/android/server/audio/AudioManagerShellCommand.java
+++ b/services/core/java/com/android/server/audio/AudioManagerShellCommand.java
@@ -53,6 +53,12 @@
                 return getSoundDoseValue();
             case "reset-sound-dose-timeout":
                 return resetSoundDoseTimeout();
+            case "set-volume":
+                return setVolume();
+            case "adj-mute":
+                return adjMute();
+            case "adj-unmute":
+                return adjUnmute();
         }
         return 0;
     }
@@ -78,6 +84,12 @@
         pw.println("    Returns the current sound dose value");
         pw.println("  reset-sound-dose-timeout");
         pw.println("    Resets the sound dose timeout used for momentary exposure");
+        pw.println("  set-volume STREAM_TYPE VOLUME_INDEX");
+        pw.println("    Sets the volume for STREAM_TYPE to VOLUME_INDEX");
+        pw.println("  adj-mute STREAM_TYPE");
+        pw.println("    mutes the STREAM_TYPE");
+        pw.println("  adj-unmute STREAM_TYPE");
+        pw.println("    unmutes the STREAM_TYPE");
     }
 
     private int setSurroundFormatEnabled() {
@@ -216,4 +228,54 @@
         getOutPrintWriter().println("Reset sound dose momentary exposure timeout");
         return 0;
     }
+
+    private int setVolume() {
+        final Context context = mService.mContext;
+        final AudioManager am = context.getSystemService(AudioManager.class);
+        final int stream = readIntArg();
+        final int index = readIntArg();
+        getOutPrintWriter().println("calling AudioManager.setStreamVolume("
+                + stream + ", " + index + ", 0)");
+        am.setStreamVolume(stream, index, 0);
+        return 0;
+    }
+
+    private int adjMute() {
+        final Context context = mService.mContext;
+        final AudioManager am = context.getSystemService(AudioManager.class);
+        final int stream = readIntArg();
+        getOutPrintWriter().println("calling AudioManager.adjustStreamVolume("
+                + stream + ", AudioManager.ADJUST_MUTE, 0)");
+        am.adjustStreamVolume(stream, AudioManager.ADJUST_MUTE, 0);
+        return 0;
+    }
+
+    private int adjUnmute() {
+        final Context context = mService.mContext;
+        final AudioManager am = context.getSystemService(AudioManager.class);
+        final int stream = readIntArg();
+        getOutPrintWriter().println("calling AudioManager.adjustStreamVolume("
+                + stream + ", AudioManager.ADJUST_UNMUTE, 0)");
+        am.adjustStreamVolume(stream, AudioManager.ADJUST_UNMUTE, 0);
+        return 0;
+    }
+
+    private int readIntArg() throws IllegalArgumentException {
+        String argText = getNextArg();
+
+        if (argText == null) {
+            getErrPrintWriter().println("Error: no argument provided");
+            throw new IllegalArgumentException("No argument provided");
+        }
+
+        int argIntVal = Integer.MIN_VALUE;
+        try {
+            argIntVal = Integer.parseInt(argText);
+        } catch (NumberFormatException e) {
+            getErrPrintWriter().println("Error: wrong format for argument " + argText);
+            throw new IllegalArgumentException("Wrong format for argument " + argText);
+        }
+
+        return argIntVal;
+    }
 }
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index ed58c40..2a79e31 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -47,6 +47,7 @@
 import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume;
 import static com.android.media.audio.Flags.ringerModeAffectsAlarm;
 import static com.android.media.audio.Flags.setStreamVolumeOrder;
+import static com.android.media.audio.Flags.vgsVssSyncMuteOrder;
 import static com.android.server.audio.SoundDoseHelper.ACTION_CHECK_MUSIC_ACTIVE;
 import static com.android.server.utils.EventLogger.Event.ALOGE;
 import static com.android.server.utils.EventLogger.Event.ALOGI;
@@ -4544,6 +4545,8 @@
                 + setStreamVolumeOrder());
         pw.println("\tandroid.media.audio.roForegroundAudioControl:"
                 + roForegroundAudioControl());
+        pw.println("\tcom.android.media.audio.vgsVssSyncMuteOrder:"
+                + vgsVssSyncMuteOrder());
     }
 
     private void dumpAudioMode(PrintWriter pw) {
@@ -8317,13 +8320,23 @@
                                         synced = true;
                                         continue;
                                     }
+                                    if (vgsVssSyncMuteOrder()) {
+                                        if ((isMuted() != streamMuted) && isVssMuteBijective(
+                                                stream)) {
+                                            mStreamStates[stream].mute(isMuted(),
+                                                    "VGS.applyAllVolumes#1");
+                                        }
+                                    }
                                     if (indexForStream != index) {
                                         mStreamStates[stream].setIndex(index * 10, device, caller,
                                                 true /*hasModifyAudioSettings*/);
                                     }
-                                    if ((isMuted() != streamMuted) && isVssMuteBijective(stream)) {
-                                        mStreamStates[stream].mute(isMuted(),
-                                                "VGS.applyAllVolumes#1");
+                                    if (!vgsVssSyncMuteOrder()) {
+                                        if ((isMuted() != streamMuted) && isVssMuteBijective(
+                                                stream)) {
+                                            mStreamStates[stream].mute(isMuted(),
+                                                    "VGS.applyAllVolumes#1");
+                                        }
                                     }
                                 }
                             }
@@ -11271,7 +11284,8 @@
         mDeviceBroker.addOrUpdateBtAudioDeviceCategoryInInventory(deviceState);
         mDeviceBroker.postPersistAudioDeviceSettings();
 
-        mSpatializerHelper.refreshDevice(deviceState.getAudioDeviceAttributes());
+        mSpatializerHelper.refreshDevice(deviceState.getAudioDeviceAttributes(),
+                false /* initState */);
         mSoundDoseHelper.setAudioDeviceCategory(addr, internalType,
                 btAudioDeviceCategory == AUDIO_DEVICE_CATEGORY_HEADPHONES);
     }
@@ -11342,11 +11356,11 @@
 
     /** Update the sound dose and spatializer state based on the new AdiDeviceState. */
     @VisibleForTesting(visibility = PACKAGE)
-    public void onUpdatedAdiDeviceState(AdiDeviceState deviceState) {
+    public void onUpdatedAdiDeviceState(AdiDeviceState deviceState, boolean initSA) {
         if (deviceState == null) {
             return;
         }
-        mSpatializerHelper.refreshDevice(deviceState.getAudioDeviceAttributes());
+        mSpatializerHelper.refreshDevice(deviceState.getAudioDeviceAttributes(), initSA);
         mSoundDoseHelper.setAudioDeviceCategory(deviceState.getDeviceAddress(),
                 deviceState.getInternalDeviceType(),
                 deviceState.getAudioDeviceCategory() == AUDIO_DEVICE_CATEGORY_HEADPHONES);
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index f3a5fdb..edeabdc 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -98,9 +98,16 @@
 
     private @Nullable BluetoothLeAudio mLeAudio;
 
+    private @Nullable BluetoothLeAudioCodecConfig mLeAudioCodecConfig;
+
     // Reference to BluetoothA2dp to query for AbsoluteVolume.
     private @Nullable BluetoothA2dp mA2dp;
 
+    private @Nullable BluetoothCodecConfig mA2dpCodecConfig;
+
+    private @AudioSystem.AudioFormatNativeEnumForBtCodec
+            int mLeAudioBroadcastCodec = AudioSystem.AUDIO_FORMAT_DEFAULT;
+
     // If absolute volume is supported in AVRCP device
     private boolean mAvrcpAbsVolSupported = false;
 
@@ -265,12 +272,15 @@
         }
     }
 
-    /*package*/ synchronized @AudioSystem.AudioFormatNativeEnumForBtCodec int getCodec(
+    private synchronized Pair<Integer, Boolean> getCodec(
             @NonNull BluetoothDevice device, @AudioService.BtProfile int profile) {
+
         switch (profile) {
             case BluetoothProfile.A2DP: {
+                boolean changed = mA2dpCodecConfig != null;
                 if (mA2dp == null) {
-                    return AudioSystem.AUDIO_FORMAT_DEFAULT;
+                    mA2dpCodecConfig = null;
+                    return new Pair<>(AudioSystem.AUDIO_FORMAT_DEFAULT, changed);
                 }
                 BluetoothCodecStatus btCodecStatus = null;
                 try {
@@ -279,17 +289,24 @@
                     Log.e(TAG, "Exception while getting status of " + device, e);
                 }
                 if (btCodecStatus == null) {
-                    return AudioSystem.AUDIO_FORMAT_DEFAULT;
+                    mA2dpCodecConfig = null;
+                    return new Pair<>(AudioSystem.AUDIO_FORMAT_DEFAULT, changed);
                 }
                 final BluetoothCodecConfig btCodecConfig = btCodecStatus.getCodecConfig();
                 if (btCodecConfig == null) {
-                    return AudioSystem.AUDIO_FORMAT_DEFAULT;
+                    mA2dpCodecConfig = null;
+                    return new Pair<>(AudioSystem.AUDIO_FORMAT_DEFAULT, changed);
                 }
-                return AudioSystem.bluetoothA2dpCodecToAudioFormat(btCodecConfig.getCodecType());
+                changed = !btCodecConfig.equals(mA2dpCodecConfig);
+                mA2dpCodecConfig = btCodecConfig;
+                return new Pair<>(AudioSystem.bluetoothA2dpCodecToAudioFormat(
+                        btCodecConfig.getCodecType()), changed);
             }
             case BluetoothProfile.LE_AUDIO: {
+                boolean changed = mLeAudioCodecConfig != null;
                 if (mLeAudio == null) {
-                    return AudioSystem.AUDIO_FORMAT_DEFAULT;
+                    mLeAudioCodecConfig = null;
+                    return new Pair<>(AudioSystem.AUDIO_FORMAT_DEFAULT, changed);
                 }
                 BluetoothLeAudioCodecStatus btLeCodecStatus = null;
                 int groupId = mLeAudio.getGroupId(device);
@@ -299,42 +316,54 @@
                     Log.e(TAG, "Exception while getting status of " + device, e);
                 }
                 if (btLeCodecStatus == null) {
-                    return AudioSystem.AUDIO_FORMAT_DEFAULT;
+                    mLeAudioCodecConfig = null;
+                    return new Pair<>(AudioSystem.AUDIO_FORMAT_DEFAULT, changed);
                 }
                 BluetoothLeAudioCodecConfig btLeCodecConfig =
                         btLeCodecStatus.getOutputCodecConfig();
                 if (btLeCodecConfig == null) {
-                    return AudioSystem.AUDIO_FORMAT_DEFAULT;
+                    mLeAudioCodecConfig = null;
+                    return new Pair<>(AudioSystem.AUDIO_FORMAT_DEFAULT, changed);
                 }
-                return AudioSystem.bluetoothLeCodecToAudioFormat(btLeCodecConfig.getCodecType());
+                changed = !btLeCodecConfig.equals(mLeAudioCodecConfig);
+                mLeAudioCodecConfig = btLeCodecConfig;
+                return new Pair<>(AudioSystem.bluetoothLeCodecToAudioFormat(
+                        btLeCodecConfig.getCodecType()), changed);
+            }
+            case BluetoothProfile.LE_AUDIO_BROADCAST: {
+                // We assume LC3 for LE Audio broadcast codec as there is no API to get the codec
+                // config on LE Broadcast profile proxy.
+                boolean changed = mLeAudioBroadcastCodec != AudioSystem.AUDIO_FORMAT_LC3;
+                mLeAudioBroadcastCodec = AudioSystem.AUDIO_FORMAT_LC3;
+                return new Pair<>(mLeAudioBroadcastCodec, changed);
             }
             default:
-                return AudioSystem.AUDIO_FORMAT_DEFAULT;
+                return new Pair<>(AudioSystem.AUDIO_FORMAT_DEFAULT, false);
         }
     }
 
-    /*package*/ synchronized @AudioSystem.AudioFormatNativeEnumForBtCodec
-            int getCodecWithFallback(
-                    @NonNull BluetoothDevice device, @AudioService.BtProfile int profile,
-                    boolean isLeOutput, @NonNull String source) {
+    /*package*/ synchronized Pair<Integer, Boolean>
+                    getCodecWithFallback(@NonNull BluetoothDevice device,
+                                         @AudioService.BtProfile int profile,
+                                         boolean isLeOutput, @NonNull String source) {
         // For profiles other than A2DP and LE Audio output, the audio codec format must be
         // AUDIO_FORMAT_DEFAULT as native audio policy manager expects a specific audio format
         // only if audio HW module selection based on format is supported for the device type.
         if (!(profile == BluetoothProfile.A2DP
                 || (isLeOutput && ((profile == BluetoothProfile.LE_AUDIO)
                         || (profile == BluetoothProfile.LE_AUDIO_BROADCAST))))) {
-            return AudioSystem.AUDIO_FORMAT_DEFAULT;
+            return new Pair<>(AudioSystem.AUDIO_FORMAT_DEFAULT, false);
         }
-        @AudioSystem.AudioFormatNativeEnumForBtCodec int codec =
+        Pair<Integer, Boolean> codecAndChanged =
                 getCodec(device, profile);
-        if (codec == AudioSystem.AUDIO_FORMAT_DEFAULT) {
+        if (codecAndChanged.first == AudioSystem.AUDIO_FORMAT_DEFAULT) {
             AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                     "getCodec DEFAULT from " + source + " fallback to "
                             + (profile == BluetoothProfile.A2DP ? "SBC" : "LC3")));
-            return profile == BluetoothProfile.A2DP
-                    ? AudioSystem.AUDIO_FORMAT_SBC : AudioSystem.AUDIO_FORMAT_LC3;
+            return new Pair<>(profile == BluetoothProfile.A2DP
+                    ? AudioSystem.AUDIO_FORMAT_SBC : AudioSystem.AUDIO_FORMAT_LC3, true);
         }
-        return codec;
+        return codecAndChanged;
     }
 
     // @GuardedBy("mDeviceBroker.mSetModeLock")
@@ -539,15 +568,19 @@
                 break;
             case BluetoothProfile.A2DP:
                 mA2dp = null;
+                mA2dpCodecConfig = null;
                 break;
             case BluetoothProfile.HEARING_AID:
                 mHearingAid = null;
                 break;
             case BluetoothProfile.LE_AUDIO:
                 mLeAudio = null;
+                mLeAudioCodecConfig = null;
+                break;
+            case BluetoothProfile.LE_AUDIO_BROADCAST:
+                mLeAudioBroadcastCodec = AudioSystem.AUDIO_FORMAT_DEFAULT;
                 break;
             case BluetoothProfile.A2DP_SINK:
-            case BluetoothProfile.LE_AUDIO_BROADCAST:
                 // nothing to do in BtHelper
                 break;
             default:
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index 3b5fa7f..38fa79f 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -295,11 +295,11 @@
             // could have been called another time after boot in case of audioserver restart
             addCompatibleAudioDevice(
                     new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, ""),
-                            false /*forceEnable*/);
+                            false /*forceEnable*/, false /*forceInit*/);
             // not force-enabling as this device might already be in the device list
             addCompatibleAudioDevice(
                     new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_WIRED_HEADPHONE, ""),
-                            false /*forceEnable*/);
+                            false /*forceEnable*/, false /*forceInit*/);
         } catch (RemoteException e) {
             resetCapabilities();
         } finally {
@@ -526,7 +526,7 @@
     }
 
     synchronized void addCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) {
-        addCompatibleAudioDevice(ada, true /*forceEnable*/);
+        addCompatibleAudioDevice(ada, true /*forceEnable*/, false /*forceInit*/);
     }
 
     /**
@@ -540,7 +540,7 @@
      */
     @GuardedBy("this")
     private void addCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada,
-            boolean forceEnable) {
+            boolean forceEnable, boolean forceInit) {
         if (!isDeviceCompatibleWithSpatializationModes(ada)) {
             return;
         }
@@ -548,6 +548,9 @@
         final AdiDeviceState deviceState = findSACompatibleDeviceStateForAudioDeviceAttributes(ada);
         AdiDeviceState updatedDevice = null; // non-null on update.
         if (deviceState != null) {
+            if (forceInit) {
+                initSAState(deviceState);
+            }
             if (forceEnable && !deviceState.isSAEnabled()) {
                 updatedDevice = deviceState;
                 updatedDevice.setSAEnabled(true);
@@ -756,10 +759,10 @@
         }
     }
 
-    synchronized void refreshDevice(@NonNull AudioDeviceAttributes ada) {
+    synchronized void refreshDevice(@NonNull AudioDeviceAttributes ada, boolean initState) {
         final AdiDeviceState deviceState = findSACompatibleDeviceStateForAudioDeviceAttributes(ada);
         if (isAvailableForAdiDeviceState(deviceState)) {
-            addCompatibleAudioDevice(ada, /*forceEnable=*/deviceState.isSAEnabled());
+            addCompatibleAudioDevice(ada, /*forceEnable=*/deviceState.isSAEnabled(), initState);
             setHeadTrackerEnabled(deviceState.isHeadTrackerEnabled(), ada);
         } else {
             removeCompatibleAudioDevice(ada);
diff --git a/services/core/java/com/android/server/biometrics/Android.bp b/services/core/java/com/android/server/biometrics/Android.bp
index 6cbe4ad..ed5de030 100644
--- a/services/core/java/com/android/server/biometrics/Android.bp
+++ b/services/core/java/com/android/server/biometrics/Android.bp
@@ -1,6 +1,7 @@
 aconfig_declarations {
     name: "biometrics_flags",
     package: "com.android.server.biometrics",
+    container: "system",
     srcs: [
         "biometrics.aconfig",
     ],
diff --git a/services/core/java/com/android/server/biometrics/biometrics.aconfig b/services/core/java/com/android/server/biometrics/biometrics.aconfig
index b12d831..7a9491e 100644
--- a/services/core/java/com/android/server/biometrics/biometrics.aconfig
+++ b/services/core/java/com/android/server/biometrics/biometrics.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.biometrics"
+container: "system"
 
 flag {
   name: "face_vhal_feature"
diff --git a/services/core/java/com/android/server/broadcastradio/BroadcastRadioService.java b/services/core/java/com/android/server/broadcastradio/BroadcastRadioService.java
index 3ede0a2..028b9b0 100644
--- a/services/core/java/com/android/server/broadcastradio/BroadcastRadioService.java
+++ b/services/core/java/com/android/server/broadcastradio/BroadcastRadioService.java
@@ -26,7 +26,7 @@
 
 import java.util.ArrayList;
 
-public class BroadcastRadioService extends SystemService {
+public final class BroadcastRadioService extends SystemService {
     private final IRadioService mServiceImpl;
 
     public BroadcastRadioService(Context context) {
diff --git a/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
index 42b2682..16514fa 100644
--- a/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
+++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
@@ -52,7 +52,7 @@
     private static final List<String> SERVICE_NAMES = Arrays.asList(
             IBroadcastRadio.DESCRIPTOR + "/amfm", IBroadcastRadio.DESCRIPTOR + "/dab");
 
-    private final BroadcastRadioServiceImpl mHalAidl;
+    private final BroadcastRadioServiceImpl mAidlHalClient;
     private final BroadcastRadioService mService;
 
     /**
@@ -77,14 +77,14 @@
     @VisibleForTesting
     IRadioServiceAidlImpl(BroadcastRadioService service, BroadcastRadioServiceImpl halAidl) {
         mService = Objects.requireNonNull(service, "Broadcast radio service cannot be null");
-        mHalAidl = Objects.requireNonNull(halAidl,
+        mAidlHalClient = Objects.requireNonNull(halAidl,
                 "Broadcast radio service implementation for AIDL HAL cannot be null");
     }
 
     @Override
     public List<RadioManager.ModuleProperties> listModules() {
         mService.enforcePolicyAccess();
-        return mHalAidl.listModules();
+        return mAidlHalClient.listModules();
     }
 
     @Override
@@ -97,7 +97,7 @@
         if (callback == null) {
             throw new IllegalArgumentException("Callback must not be null");
         }
-        return mHalAidl.openSession(moduleId, bandConfig, withAudio, callback);
+        return mAidlHalClient.openSession(moduleId, bandConfig, withAudio, callback);
     }
 
     @Override
@@ -110,7 +110,7 @@
         Objects.requireNonNull(listener, "Announcement listener cannot be null");
         mService.enforcePolicyAccess();
 
-        return mHalAidl.addAnnouncementListener(enabledTypes, listener);
+        return mAidlHalClient.addAnnouncementListener(enabledTypes, listener);
     }
 
     @Override
@@ -126,10 +126,10 @@
         radioPrintWriter.printf("BroadcastRadioService\n");
 
         radioPrintWriter.increaseIndent();
-        radioPrintWriter.printf("AIDL HAL:\n");
+        radioPrintWriter.printf("AIDL HAL client:\n");
 
         radioPrintWriter.increaseIndent();
-        mHalAidl.dumpInfo(radioPrintWriter);
+        mAidlHalClient.dumpInfo(radioPrintWriter);
         radioPrintWriter.decreaseIndent();
 
         radioPrintWriter.decreaseIndent();
diff --git a/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
index bc72a4b..ab08342 100644
--- a/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
+++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
@@ -49,8 +49,8 @@
 final class IRadioServiceHidlImpl extends IRadioService.Stub {
     private static final String TAG = "BcRadioSrvHidl";
 
-    private final com.android.server.broadcastradio.hal1.BroadcastRadioService mHal1;
-    private final com.android.server.broadcastradio.hal2.BroadcastRadioService mHal2;
+    private final com.android.server.broadcastradio.hal1.BroadcastRadioService mHal1Client;
+    private final com.android.server.broadcastradio.hal2.BroadcastRadioService mHal2Client;
 
     private final Object mLock = new Object();
 
@@ -61,10 +61,10 @@
 
     IRadioServiceHidlImpl(BroadcastRadioService service) {
         mService = Objects.requireNonNull(service, "broadcast radio service cannot be null");
-        mHal1 = new com.android.server.broadcastradio.hal1.BroadcastRadioService();
-        mV1Modules = mHal1.loadModules();
+        mHal1Client = new com.android.server.broadcastradio.hal1.BroadcastRadioService();
+        mV1Modules = mHal1Client.loadModules();
         OptionalInt max = mV1Modules.stream().mapToInt(RadioManager.ModuleProperties::getId).max();
-        mHal2 = new com.android.server.broadcastradio.hal2.BroadcastRadioService(
+        mHal2Client = new com.android.server.broadcastradio.hal2.BroadcastRadioService(
                 max.isPresent() ? max.getAsInt() + 1 : 0);
     }
 
@@ -73,17 +73,17 @@
             com.android.server.broadcastradio.hal1.BroadcastRadioService hal1,
             com.android.server.broadcastradio.hal2.BroadcastRadioService hal2) {
         mService = Objects.requireNonNull(service, "Broadcast radio service cannot be null");
-        mHal1 = Objects.requireNonNull(hal1,
+        mHal1Client = Objects.requireNonNull(hal1,
                 "Broadcast radio service implementation for HIDL 1 HAL cannot be null");
-        mV1Modules = mHal1.loadModules();
-        mHal2 = Objects.requireNonNull(hal2,
+        mV1Modules = mHal1Client.loadModules();
+        mHal2Client = Objects.requireNonNull(hal2,
                 "Broadcast radio service implementation for HIDL 2 HAL cannot be null");
     }
 
     @Override
     public List<RadioManager.ModuleProperties> listModules() {
         mService.enforcePolicyAccess();
-        Collection<RadioManager.ModuleProperties> v2Modules = mHal2.listModules();
+        Collection<RadioManager.ModuleProperties> v2Modules = mHal2Client.listModules();
         List<RadioManager.ModuleProperties> modules;
         synchronized (mLock) {
             modules = new ArrayList<>(mV1Modules.size() + v2Modules.size());
@@ -102,10 +102,10 @@
         mService.enforcePolicyAccess();
         Objects.requireNonNull(callback, "Callback must not be null");
         synchronized (mLock) {
-            if (mHal2.hasModule(moduleId)) {
-                return mHal2.openSession(moduleId, bandConfig, withAudio, callback);
+            if (mHal2Client.hasModule(moduleId)) {
+                return mHal2Client.openSession(moduleId, bandConfig, withAudio, callback);
             } else {
-                return mHal1.openTuner(moduleId, bandConfig, withAudio, callback);
+                return mHal1Client.openTuner(moduleId, bandConfig, withAudio, callback);
             }
         }
     }
@@ -121,12 +121,12 @@
         mService.enforcePolicyAccess();
 
         synchronized (mLock) {
-            if (!mHal2.hasAnyModules()) {
+            if (!mHal2Client.hasAnyModules()) {
                 Slog.w(TAG, "There are no HAL 2.0 modules registered");
                 return new AnnouncementAggregator(listener, mLock);
             }
 
-            return mHal2.addAnnouncementListener(enabledTypes, listener);
+            return mHal2Client.addAnnouncementListener(enabledTypes, listener);
         }
     }
 
@@ -143,18 +143,18 @@
         radioPw.printf("BroadcastRadioService\n");
 
         radioPw.increaseIndent();
-        radioPw.printf("HAL1: %s\n", mHal1);
+        radioPw.printf("HAL1 client: %s\n", mHal1Client);
 
         radioPw.increaseIndent();
         synchronized (mLock) {
-            radioPw.printf("Modules of HAL1: %s\n", mV1Modules);
+            radioPw.printf("Modules of HAL1 client: %s\n", mV1Modules);
         }
         radioPw.decreaseIndent();
 
-        radioPw.printf("HAL2:\n");
+        radioPw.printf("HAL2 client:\n");
 
         radioPw.increaseIndent();
-        mHal2.dumpInfo(radioPw);
+        mHal2Client.dumpInfo(radioPw);
         radioPw.decreaseIndent();
 
         radioPw.decreaseIndent();
diff --git a/services/core/java/com/android/server/broadcastradio/RadioServiceUserController.java b/services/core/java/com/android/server/broadcastradio/RadioServiceUserController.java
index 4b847a2..c705ebe 100644
--- a/services/core/java/com/android/server/broadcastradio/RadioServiceUserController.java
+++ b/services/core/java/com/android/server/broadcastradio/RadioServiceUserController.java
@@ -48,8 +48,8 @@
      * @return foreground user id.
      */
     public static int getCurrentUser() {
-        final long identity = Binder.clearCallingIdentity();
         int userId = UserHandle.USER_NULL;
+        final long identity = Binder.clearCallingIdentity();
         try {
             userId = ActivityManager.getCurrentUser();
         } catch (RuntimeException e) {
diff --git a/services/core/java/com/android/server/broadcastradio/hal1/Convert.java b/services/core/java/com/android/server/broadcastradio/hal1/Convert.java
index 219ee4c..08ff662 100644
--- a/services/core/java/com/android/server/broadcastradio/hal1/Convert.java
+++ b/services/core/java/com/android/server/broadcastradio/hal1/Convert.java
@@ -18,12 +18,13 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.util.Slog;
+
+import com.android.server.utils.Slogf;
 
 import java.util.Map;
 import java.util.Set;
 
-class Convert {
+final class Convert {
 
     private static final String TAG = "BcRadio1Srv.Convert";
 
@@ -34,12 +35,12 @@
      * side, which requires several separate java calls for each element.
      *
      * @param map map to convert.
-     * @returns array (sized the same as map) of two-element string arrays
-     *          (first element is the key, second is value).
+     * @return array (sized the same as map) of two-element string arrays
+     *         (first element is the key, second is value).
      */
     static @NonNull String[][] stringMapToNative(@Nullable Map<String, String> map) {
         if (map == null) {
-            Slog.v(TAG, "map is null, returning zero-elements array");
+            Slogf.v(TAG, "map is null, returning zero-elements array");
             return new String[0][0];
         }
 
@@ -54,7 +55,7 @@
             i++;
         }
 
-        Slog.v(TAG, "converted " + i + " element(s)");
+        Slogf.v(TAG, "converted " + i + " element(s)");
         return arr;
     }
 }
diff --git a/services/core/java/com/android/server/broadcastradio/hal1/Tuner.java b/services/core/java/com/android/server/broadcastradio/hal1/Tuner.java
index 8e5f6b5..7cac409 100644
--- a/services/core/java/com/android/server/broadcastradio/hal1/Tuner.java
+++ b/services/core/java/com/android/server/broadcastradio/hal1/Tuner.java
@@ -26,7 +26,6 @@
 import android.hardware.radio.RadioManager;
 import android.os.IBinder;
 import android.os.RemoteException;
-import android.util.Slog;
 
 import com.android.server.broadcastradio.RadioServiceUserController;
 import com.android.server.utils.Slogf;
@@ -44,9 +43,9 @@
     private final long mNativeContext;
 
     private final Object mLock = new Object();
-    @NonNull private final TunerCallback mTunerCallback;
-    @NonNull private final ITunerCallback mClientCallback;
-    @NonNull private final IBinder.DeathRecipient mDeathRecipient;
+    private final TunerCallback mTunerCallback;
+    private final ITunerCallback mClientCallback;
+    private final IBinder.DeathRecipient mDeathRecipient;
 
     private boolean mIsClosed = false;
     private boolean mIsMuted = false;
@@ -122,7 +121,7 @@
 
     private boolean checkConfiguredLocked() {
         if (mTunerCallback.isInitialConfigurationDone()) return true;
-        Slog.w(TAG, "Initial configuration is still pending, skipping the operation");
+        Slogf.w(TAG, "Initial configuration is still pending, skipping the operation");
         return false;
     }
 
@@ -159,14 +158,14 @@
             checkNotClosedLocked();
             if (mIsMuted == mute) return;
             mIsMuted = mute;
-            Slog.w(TAG, "Mute via RadioService is not implemented - please handle it via app");
+            Slogf.w(TAG, "Mute via RadioService is not implemented - please handle it via app");
         }
     }
 
     @Override
     public boolean isMuted() {
         if (!mWithAudio) {
-            Slog.w(TAG, "Tuner did not request audio, pretending it was muted");
+            Slogf.w(TAG, "Tuner did not request audio, pretending it was muted");
             return true;
         }
         synchronized (mLock) {
@@ -210,7 +209,7 @@
         if (selector == null) {
             throw new IllegalArgumentException("The argument must not be a null pointer");
         }
-        Slog.i(TAG, "Tuning to " + selector);
+        Slogf.i(TAG, "Tuning to " + selector);
         synchronized (mLock) {
             checkNotClosedLocked();
             if (!checkConfiguredLocked()) return;
diff --git a/services/core/java/com/android/server/broadcastradio/hal1/TunerCallback.java b/services/core/java/com/android/server/broadcastradio/hal1/TunerCallback.java
index aa43b75..e013643 100644
--- a/services/core/java/com/android/server/broadcastradio/hal1/TunerCallback.java
+++ b/services/core/java/com/android/server/broadcastradio/hal1/TunerCallback.java
@@ -25,7 +25,8 @@
 import android.hardware.radio.RadioTuner;
 import android.os.IBinder;
 import android.os.RemoteException;
-import android.util.Slog;
+
+import com.android.server.utils.Slogf;
 
 import java.util.List;
 import java.util.Map;
@@ -42,8 +43,8 @@
      */
     private final long mNativeContext;
 
-    @NonNull private final Tuner mTuner;
-    @NonNull private final ITunerCallback mClientCallback;
+    private final Tuner mTuner;
+    private final ITunerCallback mClientCallback;
 
     private final AtomicReference<ProgramList.Filter> mProgramListFilter = new AtomicReference<>();
     private boolean mInitialConfigurationDone = false;
@@ -76,7 +77,7 @@
         try {
             func.run();
         } catch (RemoteException e) {
-            Slog.e(TAG, "client died", e);
+            Slogf.e(TAG, "client died", e);
         }
     }
 
@@ -107,7 +108,7 @@
 
     @Override
     public void onTuneFailed(int result, ProgramSelector selector) {
-        Slog.e(TAG, "Not applicable for HAL 1.x");
+        Slogf.e(TAG, "Not applicable for HAL 1.x");
     }
 
     @Override
@@ -160,7 +161,7 @@
         try {
             modified = mTuner.getProgramList(filter.getVendorFilter());
         } catch (IllegalStateException ex) {
-            Slog.d(TAG, "Program list not ready yet");
+            Slogf.d(TAG, "Program list not ready yet");
             return;
         }
         Set<RadioManager.ProgramInfo> modifiedSet = modified.stream().collect(Collectors.toSet());
@@ -175,12 +176,12 @@
 
     @Override
     public void onConfigFlagUpdated(int flag, boolean value) {
-        Slog.w(TAG, "Not applicable for HAL 1.x");
+        Slogf.w(TAG, "Not applicable for HAL 1.x");
     }
 
     @Override
     public void onParametersUpdated(Map<String, String> parameters) {
-        Slog.w(TAG, "Not applicable for HAL 1.x");
+        Slogf.w(TAG, "Not applicable for HAL 1.x");
     }
 
     @Override
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/AnnouncementAggregator.java b/services/core/java/com/android/server/broadcastradio/hal2/AnnouncementAggregator.java
index 85c13ae..0327ee7 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/AnnouncementAggregator.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/AnnouncementAggregator.java
@@ -23,20 +23,20 @@
 import android.hardware.radio.ICloseHandle;
 import android.os.IBinder;
 import android.os.RemoteException;
-import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.server.utils.Slogf;
 
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
 
-public class AnnouncementAggregator extends ICloseHandle.Stub {
+public final class AnnouncementAggregator extends ICloseHandle.Stub {
     private static final String TAG = "BcRadio2Srv.AnnAggr";
 
     private final Object mLock;
-    @NonNull private final IAnnouncementListener mListener;
+    private final IAnnouncementListener mListener;
     private final IBinder.DeathRecipient mDeathRecipient = new DeathRecipient();
 
     @GuardedBy("mLock")
@@ -77,14 +77,16 @@
         public void binderDied() {
             try {
                 close();
-            } catch (RemoteException ex) {}
+            } catch (RemoteException ex) {
+                Slogf.e(TAG, ex, "Cannot close Announcement aggregator for DeathRecipient");
+            }
         }
     }
 
     private void onListUpdated() {
         synchronized (mLock) {
             if (mIsClosed) {
-                Slog.e(TAG, "Announcement aggregator is closed, it shouldn't receive callbacks");
+                Slogf.e(TAG, "Announcement aggregator is closed, it shouldn't receive callbacks");
                 return;
             }
             List<Announcement> combined = new ArrayList<>();
@@ -94,7 +96,7 @@
             try {
                 mListener.onListUpdated(combined);
             } catch (RemoteException ex) {
-                Slog.e(TAG, "mListener.onListUpdated() failed: ", ex);
+                Slogf.e(TAG, "mListener.onListUpdated() failed: ", ex);
             }
         }
     }
@@ -111,7 +113,7 @@
             try {
                 closeHandle = module.addAnnouncementListener(enabledTypes, watcher);
             } catch (RemoteException ex) {
-                Slog.e(TAG, "Failed to add announcement listener", ex);
+                Slogf.e(TAG, "Failed to add announcement listener", ex);
                 return;
             }
             watcher.setCloseHandle(closeHandle);
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java b/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java
index 1e31f20..3198842 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java
@@ -29,8 +29,8 @@
 import android.hidl.manager.V1_0.IServiceNotification;
 import android.os.IHwBinder.DeathRecipient;
 import android.os.RemoteException;
+import android.util.ArrayMap;
 import android.util.IndentingPrintWriter;
-import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -38,7 +38,6 @@
 import com.android.server.utils.Slogf;
 
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
 import java.util.stream.Collectors;
@@ -55,16 +54,17 @@
     private int mNextModuleId;
 
     @GuardedBy("mLock")
-    private final Map<String, Integer> mServiceNameToModuleIdMap = new HashMap<>();
+    private final Map<String, Integer> mServiceNameToModuleIdMap = new ArrayMap<>();
 
     // Map from module ID to RadioModule created by mServiceListener.onRegistration().
     @GuardedBy("mLock")
-    private final Map<Integer, RadioModule> mModules = new HashMap<>();
+    private final Map<Integer, RadioModule> mModules = new ArrayMap<>();
 
-    private IServiceNotification.Stub mServiceListener = new IServiceNotification.Stub() {
+    private final IServiceNotification.Stub mServiceListener = new IServiceNotification.Stub() {
         @Override
         public void onRegistration(String fqName, String serviceName, boolean preexisting) {
-            Slog.v(TAG, "onRegistration(" + fqName + ", " + serviceName + ", " + preexisting + ")");
+            Slogf.v(TAG, "onRegistration(" + fqName + ", " + serviceName + ", " + preexisting
+                    + ")");
             Integer moduleId;
             synchronized (mLock) {
                 // If the service has been registered before, reuse its previous module ID.
@@ -75,13 +75,13 @@
                     moduleId = mNextModuleId;
                 }
 
-                RadioModule module = RadioModule.tryLoadingModule(moduleId, serviceName);
-                if (module == null) {
+                RadioModule radioModule = RadioModule.tryLoadingModule(moduleId, serviceName);
+                if (radioModule == null) {
                     return;
                 }
-                Slog.v(TAG, "loaded broadcast radio module " + moduleId + ": " + serviceName
+                Slogf.v(TAG, "loaded broadcast radio module " + moduleId + ": " + serviceName
                         + " (HAL 2.0)");
-                RadioModule prevModule = mModules.put(moduleId, module);
+                RadioModule prevModule = mModules.put(moduleId, radioModule);
                 if (prevModule != null) {
                     prevModule.closeSessions(RadioTuner.ERROR_HARDWARE_FAILURE);
                 }
@@ -92,7 +92,7 @@
                 }
 
                 try {
-                    module.getService().linkToDeath(mDeathRecipient, moduleId);
+                    radioModule.getService().linkToDeath(mDeathRecipient, moduleId);
                 } catch (RemoteException ex) {
                     // Service has already died, so remove its entry from mModules.
                     mModules.remove(moduleId);
@@ -101,10 +101,10 @@
         }
     };
 
-    private DeathRecipient mDeathRecipient = new DeathRecipient() {
+    private final DeathRecipient mDeathRecipient = new DeathRecipient() {
         @Override
         public void serviceDied(long cookie) {
-            Slog.v(TAG, "serviceDied(" + cookie + ")");
+            Slogf.v(TAG, "serviceDied(" + cookie + ")");
             synchronized (mLock) {
                 int moduleId = (int) cookie;
                 RadioModule prevModule = mModules.remove(moduleId);
@@ -114,7 +114,7 @@
 
                 for (Map.Entry<String, Integer> entry : mServiceNameToModuleIdMap.entrySet()) {
                     if (entry.getValue() == moduleId) {
-                        Slog.i(TAG, "service " + entry.getKey()
+                        Slogf.i(TAG, "service " + entry.getKey()
                                 + " died; removed RadioModule with ID " + moduleId);
                         return;
                     }
@@ -128,12 +128,12 @@
         try {
             IServiceManager manager = IServiceManager.getService();
             if (manager == null) {
-                Slog.e(TAG, "failed to get HIDL Service Manager");
+                Slogf.e(TAG, "failed to get HIDL Service Manager");
                 return;
             }
             manager.registerForNotifications(IBroadcastRadio.kInterfaceName, "", mServiceListener);
         } catch (RemoteException ex) {
-            Slog.e(TAG, "failed to register for service notifications: ", ex);
+            Slogf.e(TAG, "failed to register for service notifications: ", ex);
         }
     }
 
@@ -144,12 +144,12 @@
         try {
             manager.registerForNotifications(IBroadcastRadio.kInterfaceName, "", mServiceListener);
         } catch (RemoteException ex) {
-            Slog.e(TAG, "Failed to register for service notifications: ", ex);
+            Slogf.e(TAG, "Failed to register for service notifications: ", ex);
         }
     }
 
     public @NonNull Collection<RadioManager.ModuleProperties> listModules() {
-        Slog.v(TAG, "List HIDL 2.0 modules");
+        Slogf.v(TAG, "List HIDL 2.0 modules");
         synchronized (mLock) {
             return mModules.values().stream().map(module -> module.getProperties())
                     .collect(Collectors.toList());
@@ -170,7 +170,7 @@
 
     public ITuner openSession(int moduleId, @Nullable RadioManager.BandConfig legacyConfig,
             boolean withAudio, @NonNull ITunerCallback callback) throws RemoteException {
-        Slog.v(TAG, "Open HIDL 2.0 session with module id " + moduleId);
+        Slogf.v(TAG, "Open HIDL 2.0 session with module id " + moduleId);
         if (!RadioServiceUserController.isCurrentOrSystemUser()) {
             Slogf.e(TAG, "Cannot open tuner on HAL 2.0 client for non-current user");
             throw new IllegalStateException("Cannot open session for non-current user");
@@ -198,7 +198,7 @@
 
     public ICloseHandle addAnnouncementListener(@NonNull int[] enabledTypes,
             @NonNull IAnnouncementListener listener) {
-        Slog.v(TAG, "Add announcementListener");
+        Slogf.v(TAG, "Add announcementListener");
         AnnouncementAggregator aggregator = new AnnouncementAggregator(listener, mLock);
         boolean anySupported = false;
         synchronized (mLock) {
@@ -207,12 +207,12 @@
                     aggregator.watchModule(module, enabledTypes);
                     anySupported = true;
                 } catch (UnsupportedOperationException ex) {
-                    Slog.v(TAG, "Announcements not supported for this module", ex);
+                    Slogf.v(TAG, "Announcements not supported for this module", ex);
                 }
             }
         }
         if (!anySupported) {
-            Slog.i(TAG, "There are no HAL modules that support announcements");
+            Slogf.i(TAG, "There are no HAL modules that support announcements");
         }
         return aggregator;
     }
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/Convert.java b/services/core/java/com/android/server/broadcastradio/hal2/Convert.java
index fb1138f..34bfa6c 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/Convert.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/Convert.java
@@ -37,21 +37,22 @@
 import android.hardware.radio.RadioMetadata;
 import android.hardware.radio.RadioTuner;
 import android.os.ParcelableException;
-import android.util.Slog;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.SparseArray;
+
+import com.android.server.utils.Slogf;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.stream.Collectors;
 
-class Convert {
+final class Convert {
 
     private static final String TAG = "BcRadio2Srv.convert";
 
@@ -111,7 +112,7 @@
             elem.key = entry.getKey();
             elem.value = entry.getValue();
             if (elem.key == null || elem.value == null) {
-                Slog.w(TAG, "VendorKeyValue contains null pointers");
+                Slogf.w(TAG, "VendorKeyValue contains null pointers");
                 continue;
             }
             list.add(elem);
@@ -120,20 +121,21 @@
         return list;
     }
 
-    static @NonNull Map<String, String>
-    vendorInfoFromHal(@Nullable List<VendorKeyValue> info) {
-        if (info == null) return Collections.emptyMap();
-
-        Map<String, String> map = new HashMap<>();
-        for (VendorKeyValue kvp : info) {
-            if (kvp.key == null || kvp.value == null) {
-                Slog.w(TAG, "VendorKeyValue contains null pointers");
-                continue;
-            }
-            map.put(kvp.key, kvp.value);
+    static @NonNull Map<String, String> vendorInfoFromHal(@Nullable List<VendorKeyValue> info) {
+        Map<String, String> vendorInfoMap = new ArrayMap<>();
+        if (info == null) {
+            return vendorInfoMap;
         }
 
-        return map;
+        for (VendorKeyValue kvp : info) {
+            if (kvp.key == null || kvp.value == null) {
+                Slogf.w(TAG, "VendorKeyValue contains null pointers");
+                continue;
+            }
+            vendorInfoMap.put(kvp.key, kvp.value);
+        }
+
+        return vendorInfoMap;
     }
 
     private static @ProgramSelector.ProgramType int identifierTypeToProgramType(
@@ -168,7 +170,7 @@
 
     private static @NonNull int[]
     identifierTypesToProgramTypes(@NonNull int[] idTypes) {
-        Set<Integer> pTypes = new HashSet<>();
+        Set<Integer> pTypes = new ArraySet<>();
 
         for (int idType : idTypes) {
             int pType = identifierTypeToProgramType(idType);
@@ -202,7 +204,7 @@
         for (AmFmBandRange range : config.ranges) {
             FrequencyBand bandType = Utils.getBand(range.lowerBound);
             if (bandType == FrequencyBand.UNKNOWN) {
-                Slog.e(TAG, "Unknown frequency band at " + range.lowerBound + "kHz");
+                Slogf.e(TAG, "Unknown frequency band at " + range.lowerBound + "kHz");
                 continue;
             }
             if (bandType == FrequencyBand.FM) {
@@ -304,7 +306,7 @@
             @NonNull android.hardware.broadcastradio.V2_0.ProgramSelector sel) {
         if (sel.primaryId.type != 0) return false;
         if (sel.primaryId.value != 0) return false;
-        if (sel.secondaryIds.size() != 0) return false;
+        if (!sel.secondaryIds.isEmpty()) return false;
         return true;
     }
 
@@ -319,7 +321,7 @@
         return new ProgramSelector(
                 identifierTypeToProgramType(sel.primaryId.type),
                 Objects.requireNonNull(programIdentifierFromHal(sel.primaryId)),
-                secondaryIds, null);
+                secondaryIds, /* vendorIds= */ null);
     }
 
     private enum MetadataType {
@@ -335,40 +337,40 @@
         }
     }
 
-    private static final Map<Integer, MetadataDef> metadataKeys;
+    private static final SparseArray<MetadataDef> METADATA_KEYS;
     static {
-        metadataKeys = new HashMap<>();
-        metadataKeys.put(MetadataKey.RDS_PS, new MetadataDef(
+        METADATA_KEYS = new SparseArray<>();
+        METADATA_KEYS.put(MetadataKey.RDS_PS, new MetadataDef(
                 MetadataType.STRING, RadioMetadata.METADATA_KEY_RDS_PS));
-        metadataKeys.put(MetadataKey.RDS_PTY, new MetadataDef(
+        METADATA_KEYS.put(MetadataKey.RDS_PTY, new MetadataDef(
                 MetadataType.INT, RadioMetadata.METADATA_KEY_RDS_PTY));
-        metadataKeys.put(MetadataKey.RBDS_PTY, new MetadataDef(
+        METADATA_KEYS.put(MetadataKey.RBDS_PTY, new MetadataDef(
                 MetadataType.INT, RadioMetadata.METADATA_KEY_RBDS_PTY));
-        metadataKeys.put(MetadataKey.RDS_RT, new MetadataDef(
+        METADATA_KEYS.put(MetadataKey.RDS_RT, new MetadataDef(
                 MetadataType.STRING, RadioMetadata.METADATA_KEY_RDS_RT));
-        metadataKeys.put(MetadataKey.SONG_TITLE, new MetadataDef(
+        METADATA_KEYS.put(MetadataKey.SONG_TITLE, new MetadataDef(
                 MetadataType.STRING, RadioMetadata.METADATA_KEY_TITLE));
-        metadataKeys.put(MetadataKey.SONG_ARTIST, new MetadataDef(
+        METADATA_KEYS.put(MetadataKey.SONG_ARTIST, new MetadataDef(
                 MetadataType.STRING, RadioMetadata.METADATA_KEY_ARTIST));
-        metadataKeys.put(MetadataKey.SONG_ALBUM, new MetadataDef(
+        METADATA_KEYS.put(MetadataKey.SONG_ALBUM, new MetadataDef(
                 MetadataType.STRING, RadioMetadata.METADATA_KEY_ALBUM));
-        metadataKeys.put(MetadataKey.STATION_ICON, new MetadataDef(
+        METADATA_KEYS.put(MetadataKey.STATION_ICON, new MetadataDef(
                 MetadataType.INT, RadioMetadata.METADATA_KEY_ICON));
-        metadataKeys.put(MetadataKey.ALBUM_ART, new MetadataDef(
+        METADATA_KEYS.put(MetadataKey.ALBUM_ART, new MetadataDef(
                 MetadataType.INT, RadioMetadata.METADATA_KEY_ART));
-        metadataKeys.put(MetadataKey.PROGRAM_NAME, new MetadataDef(
+        METADATA_KEYS.put(MetadataKey.PROGRAM_NAME, new MetadataDef(
                 MetadataType.STRING, RadioMetadata.METADATA_KEY_PROGRAM_NAME));
-        metadataKeys.put(MetadataKey.DAB_ENSEMBLE_NAME, new MetadataDef(
+        METADATA_KEYS.put(MetadataKey.DAB_ENSEMBLE_NAME, new MetadataDef(
                 MetadataType.STRING, RadioMetadata.METADATA_KEY_DAB_ENSEMBLE_NAME));
-        metadataKeys.put(MetadataKey.DAB_ENSEMBLE_NAME_SHORT, new MetadataDef(
+        METADATA_KEYS.put(MetadataKey.DAB_ENSEMBLE_NAME_SHORT, new MetadataDef(
                 MetadataType.STRING, RadioMetadata.METADATA_KEY_DAB_ENSEMBLE_NAME_SHORT));
-        metadataKeys.put(MetadataKey.DAB_SERVICE_NAME, new MetadataDef(
+        METADATA_KEYS.put(MetadataKey.DAB_SERVICE_NAME, new MetadataDef(
                 MetadataType.STRING, RadioMetadata.METADATA_KEY_DAB_SERVICE_NAME));
-        metadataKeys.put(MetadataKey.DAB_SERVICE_NAME_SHORT, new MetadataDef(
+        METADATA_KEYS.put(MetadataKey.DAB_SERVICE_NAME_SHORT, new MetadataDef(
                 MetadataType.STRING, RadioMetadata.METADATA_KEY_DAB_SERVICE_NAME_SHORT));
-        metadataKeys.put(MetadataKey.DAB_COMPONENT_NAME, new MetadataDef(
+        METADATA_KEYS.put(MetadataKey.DAB_COMPONENT_NAME, new MetadataDef(
                 MetadataType.STRING, RadioMetadata.METADATA_KEY_DAB_COMPONENT_NAME));
-        metadataKeys.put(MetadataKey.DAB_COMPONENT_NAME_SHORT, new MetadataDef(
+        METADATA_KEYS.put(MetadataKey.DAB_COMPONENT_NAME_SHORT, new MetadataDef(
                 MetadataType.STRING, RadioMetadata.METADATA_KEY_DAB_COMPONENT_NAME_SHORT));
     }
 
@@ -376,9 +378,9 @@
         RadioMetadata.Builder builder = new RadioMetadata.Builder();
 
         for (Metadata entry : meta) {
-            MetadataDef keyDef = metadataKeys.get(entry.key);
+            MetadataDef keyDef = METADATA_KEYS.get(entry.key);
             if (keyDef == null) {
-                Slog.i(TAG, "Ignored unknown metadata entry: " + MetadataKey.toString(entry.key));
+                Slogf.i(TAG, "Ignored unknown metadata entry: " + MetadataKey.toString(entry.key));
                 continue;
             }
             if (keyDef.type == MetadataType.STRING) {
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/RadioEventLogger.java b/services/core/java/com/android/server/broadcastradio/hal2/RadioEventLogger.java
index 48112c4..b8d1228 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/RadioEventLogger.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/RadioEventLogger.java
@@ -19,7 +19,8 @@
 import android.util.IndentingPrintWriter;
 import android.util.LocalLog;
 import android.util.Log;
-import android.util.Slog;
+
+import com.android.server.utils.Slogf;
 
 final class RadioEventLogger {
     private final String mTag;
@@ -30,11 +31,12 @@
         mEventLogger = new LocalLog(loggerQueueSize);
     }
 
+    @SuppressWarnings("AnnotateFormatMethod")
     void logRadioEvent(String logFormat, Object... args) {
         String log = String.format(logFormat, args);
         mEventLogger.log(log);
         if (Log.isLoggable(mTag, Log.DEBUG)) {
-            Slog.d(mTag, log);
+            Slogf.d(mTag, log);
         }
     }
 
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java b/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java
index a54af2e..0e11df8 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java
@@ -39,16 +39,16 @@
 import android.os.Looper;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.util.ArraySet;
 import android.util.IndentingPrintWriter;
 import android.util.MutableInt;
-import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.broadcastradio.RadioServiceUserController;
+import com.android.server.utils.Slogf;
 
 import java.util.ArrayList;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -59,12 +59,12 @@
     private static final String TAG = "BcRadio2Srv.module";
     private static final int RADIO_EVENT_LOGGER_QUEUE_SIZE = 25;
 
-    @NonNull private final IBroadcastRadio mService;
-    @NonNull private final RadioManager.ModuleProperties mProperties;
+    private final IBroadcastRadio mService;
+    private final RadioManager.ModuleProperties mProperties;
 
     private final Object mLock = new Object();
-    @NonNull private final Handler mHandler;
-    @NonNull private final RadioEventLogger mEventLogger;
+    private final Handler mHandler;
+    private final RadioEventLogger mEventLogger;
 
     @GuardedBy("mLock")
     private ITunerSession mHalTunerSession;
@@ -144,7 +144,7 @@
 
     // Collection of active AIDL tuner sessions created through openSession().
     @GuardedBy("mLock")
-    private final Set<TunerSession> mAidlTunerSessions = new HashSet<>();
+    private final Set<TunerSession> mAidlTunerSessions = new ArraySet<>();
 
     @VisibleForTesting
     RadioModule(@NonNull IBroadcastRadio service,
@@ -158,10 +158,10 @@
     @Nullable
     static RadioModule tryLoadingModule(int idx, @NonNull String fqName) {
         try {
-            Slog.i(TAG, "Try loading module for idx " + idx + ", fqName " + fqName);
+            Slogf.i(TAG, "Try loading module for idx " + idx + ", fqName " + fqName);
             IBroadcastRadio service = IBroadcastRadio.getService(fqName);
             if (service == null) {
-                Slog.w(TAG, "No service found for fqName " + fqName);
+                Slogf.w(TAG, "No service found for fqName " + fqName);
                 return null;
             }
 
@@ -180,7 +180,7 @@
 
             return new RadioModule(service, prop);
         } catch (RemoteException ex) {
-            Slog.e(TAG, "Failed to load module " + fqName, ex);
+            Slogf.e(TAG, "Failed to load module " + fqName, ex);
             return null;
         }
     }
@@ -256,8 +256,8 @@
             }
 
             if (idTypes == null) {
-                idTypes = new HashSet<>(filter.getIdentifierTypes());
-                ids = new HashSet<>(filter.getIdentifiers());
+                idTypes = new ArraySet<>(filter.getIdentifierTypes());
+                ids = new ArraySet<>(filter.getIdentifiers());
                 includeCategories = filter.areCategoriesIncluded();
                 excludeModifications = filter.areModificationsExcluded();
                 continue;
@@ -305,7 +305,7 @@
             try {
                 mHalTunerSession.stopProgramListUpdates();
             } catch (RemoteException ex) {
-                Slog.e(TAG, "mHalTunerSession.stopProgramListUpdates() failed: ", ex);
+                Slogf.e(TAG, "mHalTunerSession.stopProgramListUpdates() failed: ", ex);
             }
             return;
         }
@@ -327,7 +327,7 @@
                     newFilter));
             Convert.throwOnError("startProgramListUpdates", halResult);
         } catch (RemoteException ex) {
-            Slog.e(TAG, "mHalTunerSession.startProgramListUpdates() failed: ", ex);
+            Slogf.e(TAG, "mHalTunerSession.startProgramListUpdates() failed: ", ex);
         }
     }
 
@@ -348,7 +348,7 @@
             try {
                 mHalTunerSession.close();
             } catch (RemoteException ex) {
-                Slog.e(TAG, "mHalTunerSession.close() failed: ", ex);
+                Slogf.e(TAG, "mHalTunerSession.close() failed: ", ex);
             }
             mHalTunerSession = null;
         }
@@ -385,18 +385,17 @@
                 runnable.run(tunerSession.mCallback);
             } catch (DeadObjectException ex) {
                 // The other side died without calling close(), so just purge it from our records.
-                Slog.e(TAG, "Removing dead TunerSession");
+                Slogf.e(TAG, "Removing dead TunerSession");
                 if (deadSessions == null) {
                     deadSessions = new ArrayList<>();
                 }
                 deadSessions.add(tunerSession);
             } catch (RemoteException ex) {
-                Slog.e(TAG, "Failed to invoke ITunerCallback: ", ex);
+                Slogf.e(TAG, "Failed to invoke ITunerCallback: ", ex);
             }
         }
         if (deadSessions != null) {
-            onTunerSessionsClosedLocked(deadSessions.toArray(
-                    new TunerSession[deadSessions.size()]));
+            onTunerSessionsClosedLocked(deadSessions.toArray(new TunerSession[0]));
         }
     }
 
@@ -429,7 +428,7 @@
                 try {
                     hwCloseHandle.value.close();
                 } catch (RemoteException ex) {
-                    Slog.e(TAG, "Failed closing announcement listener", ex);
+                    Slogf.e(TAG, "Failed closing announcement listener", ex);
                 }
                 hwCloseHandle.value = null;
             }
@@ -447,7 +446,9 @@
             rawImage[i] = rawList.get(i);
         }
 
-        if (rawImage == null || rawImage.length == 0) return null;
+        if (rawImage.length == 0) {
+            return null;
+        }
 
         return BitmapFactory.decodeByteArray(rawImage, 0, rawImage.length);
     }
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java b/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java
index 978dc01..6d435e3 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java
@@ -30,27 +30,25 @@
 import android.os.Binder;
 import android.os.RemoteException;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.IndentingPrintWriter;
 import android.util.MutableBoolean;
 import android.util.MutableInt;
-import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.server.broadcastradio.RadioServiceUserController;
 import com.android.server.utils.Slogf;
 
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 
-class TunerSession extends ITuner.Stub {
+final class TunerSession extends ITuner.Stub {
     private static final String TAG = "BcRadio2Srv.session";
-    private static final String kAudioDeviceName = "Radio tuner source";
     private static final int TUNER_EVENT_LOGGER_QUEUE_SIZE = 25;
 
     private final Object mLock = new Object();
-    @NonNull private final RadioEventLogger mEventLogger;
+    private final RadioEventLogger mEventLogger;
 
     private final RadioModule mModule;
     private final ITunerSession mHwSession;
@@ -99,7 +97,7 @@
             try {
                 mCallback.onError(error);
             } catch (RemoteException ex) {
-                Slog.w(TAG, "mCallback.onError() failed: ", ex);
+                Slogf.w(TAG, "mCallback.onError() failed: ", ex);
             }
         }
         mModule.onTunerSessionClosed(this);
@@ -129,7 +127,7 @@
             checkNotClosedLocked();
             mDummyConfig = Objects.requireNonNull(config);
         }
-        Slog.i(TAG, "Ignoring setConfiguration - not applicable for broadcastradio HAL 2.0");
+        Slogf.i(TAG, "Ignoring setConfiguration - not applicable for broadcastradio HAL 2.0");
         mModule.fanoutAidlCallback(cb -> cb.onConfigurationChanged(config));
     }
 
@@ -148,7 +146,7 @@
             if (mIsMuted == mute) return;
             mIsMuted = mute;
         }
-        Slog.w(TAG, "Mute via RadioService is not implemented - please handle it via app");
+        Slogf.w(TAG, "Mute via RadioService is not implemented - please handle it via app");
     }
 
     @Override
@@ -205,7 +203,7 @@
 
     @Override
     public void cancel() {
-        Slog.i(TAG, "Cancel");
+        Slogf.i(TAG, "Cancel");
         if (!RadioServiceUserController.isCurrentOrSystemUser()) {
             Slogf.w(TAG, "Cannot cancel on HAL 2.0 client from non-current user");
             return;
@@ -218,7 +216,8 @@
 
     @Override
     public void cancelAnnouncement() {
-        Slog.w(TAG, "Announcements control doesn't involve cancelling at the HAL level in HAL 2.0");
+        Slogf.w(TAG,
+                "Announcements control doesn't involve cancelling at the HAL level in HAL 2.0");
     }
 
     @Override
@@ -229,7 +228,7 @@
 
     @Override
     public boolean startBackgroundScan() {
-        Slog.w(TAG, "Explicit background scan trigger is not supported with HAL 2.0");
+        Slogf.w(TAG, "Explicit background scan trigger is not supported with HAL 2.0");
         if (!RadioServiceUserController.isCurrentOrSystemUser()) {
             Slogf.w(TAG,
                     "Cannot start background scan on HAL 2.0 client from non-current user");
@@ -240,7 +239,7 @@
     }
 
     @Override
-    public void startProgramListUpdates(ProgramList.Filter filter) throws RemoteException {
+    public void startProgramListUpdates(ProgramList.Filter filter) {
         mEventLogger.logRadioEvent("start programList updates %s", filter);
         if (!RadioServiceUserController.isCurrentOrSystemUser()) {
             Slogf.w(TAG,
@@ -250,8 +249,8 @@
         // If the AIDL client provides a null filter, it wants all updates, so use the most broad
         // filter.
         if (filter == null) {
-            filter = new ProgramList.Filter(new HashSet<Integer>(),
-                    new HashSet<android.hardware.radio.ProgramSelector.Identifier>(), true, false);
+            filter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(),
+                    /* includeCategories= */ true, /* excludeModifications= */ false);
         }
         synchronized (mLock) {
             checkNotClosedLocked();
@@ -285,7 +284,7 @@
             if (mProgramInfoCache == null) {
                 return;
             }
-            clientUpdateChunks = mProgramInfoCache.filterAndUpdateFrom(halCache, true);
+            clientUpdateChunks = mProgramInfoCache.filterAndUpdateFrom(halCache, /* purge= */ true);
         }
         dispatchClientUpdateChunks(clientUpdateChunks);
     }
@@ -298,7 +297,7 @@
             try {
                 mCallback.onProgramListUpdated(chunk);
             } catch (RemoteException ex) {
-                Slog.w(TAG, "mCallback.onProgramListUpdated() failed: ", ex);
+                Slogf.w(TAG, "mCallback.onProgramListUpdated() failed: ", ex);
             }
         }
     }
diff --git a/services/core/java/com/android/server/display/BrightnessRangeController.java b/services/core/java/com/android/server/display/BrightnessRangeController.java
index 10030b3..dc0e80c 100644
--- a/services/core/java/com/android/server/display/BrightnessRangeController.java
+++ b/services/core/java/com/android/server/display/BrightnessRangeController.java
@@ -117,6 +117,7 @@
                 () -> mNormalBrightnessModeController.setAutoBrightnessState(state),
                 () ->  mHbmController.setAutoBrightnessEnabled(state)
         );
+        mHdrClamper.setAutoBrightnessState(state);
     }
 
     void onBrightnessChanged(float brightness, float unthrottledBrightness,
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 40f0362..31092f2 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -757,6 +757,7 @@
             mContext.getSystemService(DeviceStateManager.class).registerCallback(
                     new HandlerExecutor(mHandler), new DeviceStateListener());
 
+            mLogicalDisplayMapper.onWindowManagerReady();
             scheduleTraversalLocked(false);
         }
     }
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index 6203a32..bca53cf 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -41,10 +41,12 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.foldables.FoldGracePeriodProvider;
+import com.android.server.LocalServices;
 import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.layout.DisplayIdProducer;
 import com.android.server.display.layout.Layout;
 import com.android.server.display.utils.DebugUtils;
+import com.android.server.policy.WindowManagerPolicy;
 import com.android.server.utils.FoldSettingProvider;
 
 import java.io.PrintWriter;
@@ -189,6 +191,7 @@
      * #updateLogicalDisplaysLocked} to establish which Virtual Devices own which Virtual Displays.
      */
     private final ArrayMap<String, Integer> mVirtualDeviceDisplayMapping = new ArrayMap<>();
+    private WindowManagerPolicy mWindowManagerPolicy;
 
     private int mNextNonDefaultGroupId = Display.DEFAULT_DISPLAY_GROUP + 1;
     private final DisplayIdProducer mIdProducer = (isDefault) ->
@@ -274,6 +277,10 @@
         mListener.onTraversalRequested();
     }
 
+    public void onWindowManagerReady() {
+        mWindowManagerPolicy = LocalServices.getService(WindowManagerPolicy.class);
+    }
+
     public LogicalDisplay getDisplayLocked(int displayId) {
         return getDisplayLocked(displayId, /* includeDisabled= */ true);
     }
@@ -1114,14 +1121,22 @@
             final int logicalDisplayId = displayLayout.getLogicalDisplayId();
 
             LogicalDisplay newDisplay = getDisplayLocked(logicalDisplayId);
+            boolean newDisplayCreated = false;
             if (newDisplay == null) {
                 newDisplay = createNewLogicalDisplayLocked(
                         null /*displayDevice*/, logicalDisplayId);
+                newDisplayCreated = true;
             }
 
             // Now swap the underlying display devices between the old display and the new display
             final LogicalDisplay oldDisplay = getDisplayLocked(device);
             if (newDisplay != oldDisplay) {
+                // Display is swapping, notify WindowManager, so it can prepare for
+                // the display switch
+                if (!newDisplayCreated && mWindowManagerPolicy != null) {
+                    mWindowManagerPolicy.onDisplaySwitchStart(newDisplay.getDisplayIdLocked());
+                }
+
                 newDisplay.swapDisplaysLocked(oldDisplay);
             }
             DisplayDeviceConfig config = device.getDisplayDeviceConfig();
diff --git a/services/core/java/com/android/server/display/NormalBrightnessModeController.java b/services/core/java/com/android/server/display/NormalBrightnessModeController.java
index 135ebd8..e94cf00 100644
--- a/services/core/java/com/android/server/display/NormalBrightnessModeController.java
+++ b/services/core/java/com/android/server/display/NormalBrightnessModeController.java
@@ -79,10 +79,12 @@
             maxBrightnessPoints = mMaxBrightnessLimits.get(BrightnessLimitMapType.ADAPTIVE);
         }
 
-        if (maxBrightnessPoints == null) {
+        // AutoBrightnessController sends ambientLux values *only* when auto brightness enabled.
+        // Temporary disabling this Controller if auto brightness is off, to avoid capping
+        // brightness based on stale ambient lux. The issue is tracked here: b/322445088
+        if (mAutoBrightnessEnabled && maxBrightnessPoints == null) {
             maxBrightnessPoints = mMaxBrightnessLimits.get(BrightnessLimitMapType.DEFAULT);
         }
-
         if (maxBrightnessPoints != null) {
             for (Map.Entry<Float, Float> brightnessPoint : maxBrightnessPoints.entrySet()) {
                 float ambientBoundary = brightnessPoint.getKey();
diff --git a/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java b/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java
index 01a8d360a..f1cb66c 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java
@@ -24,6 +24,7 @@
 import android.view.SurfaceControlHdrLayerInfoListener;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.display.AutomaticBrightnessController;
 import com.android.server.display.config.HdrBrightnessData;
 
 import java.io.PrintWriter;
@@ -56,6 +57,8 @@
     private float mTransitionRate = -1f;
     private float mDesiredTransitionRate = -1f;
 
+    private boolean mAutoBrightnessEnabled = false;
+
     public HdrClamper(BrightnessClamperController.ClamperChangeListener clamperChangeListener,
             Handler handler) {
         this(clamperChangeListener, handler, new Injector());
@@ -122,6 +125,18 @@
         recalculateBrightnessCap(data, mAmbientLux, mHdrVisible);
     }
 
+    /**
+     * Sets state of auto brightness to temporary disabling this Clamper if auto brightness is off.
+     * The issue is tracked here: b/322445088
+     */
+    public void setAutoBrightnessState(int state) {
+        boolean isEnabled = state == AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED;
+        if (isEnabled != mAutoBrightnessEnabled) {
+            mAutoBrightnessEnabled = isEnabled;
+            recalculateBrightnessCap(mHdrBrightnessData, mAmbientLux, mHdrVisible);
+        }
+    }
+
     /** Clean up all resources */
     @SuppressLint("AndroidFrameworkRequiresPermission")
     public void stop() {
@@ -145,6 +160,7 @@
                 : mHdrBrightnessData.toString()));
         pw.println("  mHdrListener registered=" + (mRegisteredDisplayToken != null));
         pw.println("  mAmbientLux=" + mAmbientLux);
+        pw.println("  mAutoBrightnessEnabled=" + mAutoBrightnessEnabled);
     }
 
     private void reset() {
@@ -163,7 +179,10 @@
 
     private void recalculateBrightnessCap(HdrBrightnessData data, float ambientLux,
             boolean hdrVisible) {
-        if (data == null || !hdrVisible) {
+        // AutoBrightnessController sends ambientLux values *only* when auto brightness enabled.
+        // Temporary disabling this Clamper if auto brightness is off, to avoid capping
+        // brightness based on stale ambient lux. The issue is tracked here: b/322445088
+        if (data == null || !hdrVisible || !mAutoBrightnessEnabled) {
             reset();
             return;
         }
diff --git a/services/core/java/com/android/server/display/feature/Android.bp b/services/core/java/com/android/server/display/feature/Android.bp
index a0ead38..daf8832 100644
--- a/services/core/java/com/android/server/display/feature/Android.bp
+++ b/services/core/java/com/android/server/display/feature/Android.bp
@@ -1,6 +1,7 @@
 aconfig_declarations {
     name: "display_flags",
     package: "com.android.server.display.feature.flags",
+    container: "system",
     srcs: [
         "*.aconfig",
     ],
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index d4319cc..c68ef9b 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.display.feature.flags"
+container: "system"
 
 # Important: Flags must be accessed through DisplayManagerFlags.
 
diff --git a/services/core/java/com/android/server/feature/Android.bp b/services/core/java/com/android/server/feature/Android.bp
index 067288d..b0fbab6 100644
--- a/services/core/java/com/android/server/feature/Android.bp
+++ b/services/core/java/com/android/server/feature/Android.bp
@@ -1,6 +1,7 @@
 aconfig_declarations {
     name: "dropbox_flags",
     package: "com.android.server.feature.flags",
+    container: "system",
     srcs: [
         "dropbox_flags.aconfig",
     ],
diff --git a/services/core/java/com/android/server/feature/dropbox_flags.aconfig b/services/core/java/com/android/server/feature/dropbox_flags.aconfig
index 14e964b..98978f0 100644
--- a/services/core/java/com/android/server/feature/dropbox_flags.aconfig
+++ b/services/core/java/com/android/server/feature/dropbox_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.feature.flags"
+container: "system"
 
 flag{
     name: "enable_read_dropbox_permission"
diff --git a/services/core/java/com/android/server/media/AudioManagerRouteController.java b/services/core/java/com/android/server/media/AudioManagerRouteController.java
index e7f717a..f27ade4 100644
--- a/services/core/java/com/android/server/media/AudioManagerRouteController.java
+++ b/services/core/java/com/android/server/media/AudioManagerRouteController.java
@@ -136,7 +136,7 @@
 
         mBluetoothRouteController =
                 new BluetoothDeviceRoutesManager(
-                        mContext, btAdapter, this::rebuildAvailableRoutesAndNotify);
+                        mContext, mHandler, btAdapter, this::rebuildAvailableRoutesAndNotify);
         // Just build routes but don't notify. The caller may not expect the listener to be invoked
         // before this constructor has finished executing.
         rebuildAvailableRoutes();
@@ -204,23 +204,24 @@
             Slog.w(TAG, "transferTo: Ignoring transfer request to unknown route id : " + routeId);
             return;
         }
-        // TODO: b/329929065 - Push audio manager and bluetooth operations to the handler, so that
-        // they don't run on a binder thread, so as to prevent possible deadlocks (these operations
-        // may need system_server binder threads to complete).
-        if (mediaRoute2InfoHolder.mCorrespondsToInactiveBluetoothRoute) {
-            // By default, the last connected device is the active route so we don't need to apply a
-            // routing audio policy.
-            mBluetoothRouteController.activateBluetoothDeviceWithAddress(
-                    mediaRoute2InfoHolder.mMediaRoute2Info.getAddress());
-            mAudioManager.removePreferredDeviceForStrategy(mStrategyForMedia);
-        } else {
-            AudioDeviceAttributes attr =
-                    new AudioDeviceAttributes(
-                            AudioDeviceAttributes.ROLE_OUTPUT,
-                            mediaRoute2InfoHolder.mAudioDeviceInfoType,
-                            /* address= */ ""); // This is not a BT device, hence no address needed.
-            mAudioManager.setPreferredDeviceForStrategy(mStrategyForMedia, attr);
-        }
+        Runnable transferAction = getTransferActionForRoute(mediaRoute2InfoHolder);
+        Runnable guardedTransferAction =
+                () -> {
+                    try {
+                        transferAction.run();
+                    } catch (Throwable throwable) {
+                        // We swallow the exception to avoid crashing system_server, since this
+                        // doesn't run on a binder thread.
+                        Slog.e(
+                                TAG,
+                                "Unexpected exception while transferring to route id: " + routeId,
+                                throwable);
+                        mHandler.post(this::rebuildAvailableRoutesAndNotify);
+                    }
+                };
+        // We post the transfer operation to the handler to avoid making these calls on a binder
+        // thread. See class javadoc for details.
+        mHandler.post(guardedTransferAction);
     }
 
     @RequiresPermission(
@@ -236,6 +237,28 @@
         return true;
     }
 
+    private Runnable getTransferActionForRoute(MediaRoute2InfoHolder mediaRoute2InfoHolder) {
+        if (mediaRoute2InfoHolder.mCorrespondsToInactiveBluetoothRoute) {
+            String deviceAddress = mediaRoute2InfoHolder.mMediaRoute2Info.getAddress();
+            return () -> {
+                // By default, the last connected device is the active route so we don't
+                // need to apply a routing audio policy.
+                mBluetoothRouteController.activateBluetoothDeviceWithAddress(deviceAddress);
+                mAudioManager.removePreferredDeviceForStrategy(mStrategyForMedia);
+            };
+
+        } else {
+            AudioDeviceAttributes deviceAttributes =
+                    new AudioDeviceAttributes(
+                            AudioDeviceAttributes.ROLE_OUTPUT,
+                            mediaRoute2InfoHolder.mAudioDeviceInfoType,
+                            /* address= */ ""); // This is not a BT device, hence no address needed.
+            return () ->
+                    mAudioManager.setPreferredDeviceForStrategy(
+                            mStrategyForMedia, deviceAttributes);
+        }
+    }
+
     @RequiresPermission(
             anyOf = {
                 Manifest.permission.MODIFY_AUDIO_ROUTING,
diff --git a/services/core/java/com/android/server/media/BluetoothDeviceRoutesManager.java b/services/core/java/com/android/server/media/BluetoothDeviceRoutesManager.java
index b881ef6..8b65ea3 100644
--- a/services/core/java/com/android/server/media/BluetoothDeviceRoutesManager.java
+++ b/services/core/java/com/android/server/media/BluetoothDeviceRoutesManager.java
@@ -31,6 +31,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.media.MediaRoute2Info;
+import android.os.Handler;
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.Log;
@@ -77,26 +78,35 @@
 
     @NonNull
     private final Context mContext;
-    @NonNull
-    private final BluetoothAdapter mBluetoothAdapter;
+    @NonNull private final Handler mHandler;
+    @NonNull private final BluetoothAdapter mBluetoothAdapter;
     @NonNull
     private final BluetoothRouteController.BluetoothRoutesUpdatedListener mListener;
     @NonNull
     private final BluetoothProfileMonitor mBluetoothProfileMonitor;
 
-    BluetoothDeviceRoutesManager(@NonNull Context context,
+    BluetoothDeviceRoutesManager(
+            @NonNull Context context,
+            @NonNull Handler handler,
             @NonNull BluetoothAdapter bluetoothAdapter,
             @NonNull BluetoothRouteController.BluetoothRoutesUpdatedListener listener) {
-        this(context, bluetoothAdapter,
-                new BluetoothProfileMonitor(context, bluetoothAdapter), listener);
+        this(
+                context,
+                handler,
+                bluetoothAdapter,
+                new BluetoothProfileMonitor(context, bluetoothAdapter),
+                listener);
     }
 
     @VisibleForTesting
-    BluetoothDeviceRoutesManager(@NonNull Context context,
+    BluetoothDeviceRoutesManager(
+            @NonNull Context context,
+            @NonNull Handler handler,
             @NonNull BluetoothAdapter bluetoothAdapter,
             @NonNull BluetoothProfileMonitor bluetoothProfileMonitor,
             @NonNull BluetoothRouteController.BluetoothRoutesUpdatedListener listener) {
         mContext = Objects.requireNonNull(context);
+        mHandler = handler;
         mBluetoothAdapter = Objects.requireNonNull(bluetoothAdapter);
         mBluetoothProfileMonitor = Objects.requireNonNull(bluetoothProfileMonitor);
         mListener = Objects.requireNonNull(listener);
@@ -298,6 +308,26 @@
         };
     }
 
+    private void handleBluetoothAdapterStateChange(int state) {
+        if (state == BluetoothAdapter.STATE_OFF || state == BluetoothAdapter.STATE_TURNING_OFF) {
+            synchronized (BluetoothDeviceRoutesManager.this) {
+                mBluetoothRoutes.clear();
+            }
+            notifyBluetoothRoutesUpdated();
+        } else if (state == BluetoothAdapter.STATE_ON) {
+            updateBluetoothRoutes();
+
+            boolean shouldCallListener;
+            synchronized (BluetoothDeviceRoutesManager.this) {
+                shouldCallListener = !mBluetoothRoutes.isEmpty();
+            }
+
+            if (shouldCallListener) {
+                notifyBluetoothRoutesUpdated();
+            }
+        }
+    }
+
     private static class BluetoothRouteInfo {
         private BluetoothDevice mBtDevice;
         private MediaRoute2Info mRoute;
@@ -308,23 +338,10 @@
         @Override
         public void onReceive(Context context, Intent intent) {
             int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
-            if (state == BluetoothAdapter.STATE_OFF
-                    || state == BluetoothAdapter.STATE_TURNING_OFF) {
-                synchronized (BluetoothDeviceRoutesManager.this) {
-                    mBluetoothRoutes.clear();
-                }
-                notifyBluetoothRoutesUpdated();
-            } else if (state == BluetoothAdapter.STATE_ON) {
-                updateBluetoothRoutes();
-
-                boolean shouldCallListener;
-                synchronized (BluetoothDeviceRoutesManager.this) {
-                    shouldCallListener = !mBluetoothRoutes.isEmpty();
-                }
-
-                if (shouldCallListener) {
-                    notifyBluetoothRoutesUpdated();
-                }
+            if (Flags.enableMr2ServiceNonMainBgThread()) {
+                mHandler.post(() -> handleBluetoothAdapterStateChange(state));
+            } else {
+                handleBluetoothAdapterStateChange(state);
             }
         }
     }
@@ -337,8 +354,16 @@
                 case BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED:
                 case BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED:
                 case BluetoothDevice.ACTION_ALIAS_CHANGED:
-                    updateBluetoothRoutes();
-                    notifyBluetoothRoutesUpdated();
+                    if (Flags.enableMr2ServiceNonMainBgThread()) {
+                        mHandler.post(
+                                () -> {
+                                    updateBluetoothRoutes();
+                                    notifyBluetoothRoutesUpdated();
+                                });
+                    } else {
+                        updateBluetoothRoutes();
+                        notifyBluetoothRoutesUpdated();
+                    }
             }
         }
     }
diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
index 67d3fe9..db83d4b 100644
--- a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
+++ b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
@@ -232,7 +232,9 @@
         if (!mRunning) {
             return false;
         }
-        if (!getSessionInfos().isEmpty() || mIsManagerScanning) {
+        boolean bindDueToManagerScan =
+                mIsManagerScanning && Flags.enablePreventionOfManagerScansWhenNoAppsScan();
+        if (!getSessionInfos().isEmpty() || bindDueToManagerScan) {
             // We bind if any manager is scanning (regardless of whether an app is scanning) to give
             // the opportunity for providers to publish routing sessions that were established
             // directly between the app and the provider (typically via AndroidX MediaRouter). See
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index aa71e05..e50189b 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -189,12 +189,10 @@
         mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
         mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
 
-        if (!Flags.disableScreenOffBroadcastReceiver()) {
-            IntentFilter screenOnOffIntentFilter = new IntentFilter();
-            screenOnOffIntentFilter.addAction(ACTION_SCREEN_ON);
-            screenOnOffIntentFilter.addAction(ACTION_SCREEN_OFF);
-            mContext.registerReceiver(mScreenOnOffReceiver, screenOnOffIntentFilter);
-        }
+        IntentFilter screenOnOffIntentFilter = new IntentFilter();
+        screenOnOffIntentFilter.addAction(ACTION_SCREEN_ON);
+        screenOnOffIntentFilter.addAction(ACTION_SCREEN_OFF);
+        mContext.registerReceiver(mScreenOnOffReceiver, screenOnOffIntentFilter);
 
         // Passing null package name to listen to all events.
         mAppOpsManager.startWatchingMode(
@@ -3435,9 +3433,7 @@
         @NonNull
         private static List<RouterRecord> getIndividuallyActiveRouters(
                 MediaRouter2ServiceImpl service, List<RouterRecord> allRouterRecords) {
-            if (!Flags.disableScreenOffBroadcastReceiver()
-                    && !service.mPowerManager.isInteractive()
-                    && !Flags.enableScreenOffScanning()) {
+            if (!service.mPowerManager.isInteractive() && !Flags.enableScreenOffScanning()) {
                 return Collections.emptyList();
             }
 
@@ -3453,9 +3449,7 @@
 
         private static boolean areManagersScanning(
                 MediaRouter2ServiceImpl service, List<ManagerRecord> managerRecords) {
-            if (!Flags.disableScreenOffBroadcastReceiver()
-                    && !service.mPowerManager.isInteractive()
-                    && !Flags.enableScreenOffScanning()) {
+            if (!service.mPowerManager.isInteractive() && !Flags.enableScreenOffScanning()) {
                 return false;
             }
 
diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java
index 4bdca29..1a129cb 100644
--- a/services/core/java/com/android/server/media/MediaRouterService.java
+++ b/services/core/java/com/android/server/media/MediaRouterService.java
@@ -53,6 +53,7 @@
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
@@ -70,6 +71,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.DumpUtils;
+import com.android.media.flags.Flags;
 import com.android.server.LocalServices;
 import com.android.server.Watchdog;
 import com.android.server.pm.UserManagerInternal;
@@ -94,6 +96,7 @@
         implements Watchdog.Monitor {
     private static final String TAG = "MediaRouterService";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+    private static final String WORKER_THREAD_NAME = "MediaRouterServiceThread";
 
     /**
      * Timeout in milliseconds for a selected route to transition from a disconnected state to a
@@ -126,7 +129,7 @@
 
     private final IAudioService mAudioService;
     private final AudioPlayerStateMonitor mAudioPlayerStateMonitor;
-    private final Handler mHandler = new Handler();
+    private final Handler mHandler;
     private final IntArray mActivePlayerMinPriorityQueue = new IntArray();
     private final IntArray mActivePlayerUidMinPriorityQueue = new IntArray();
 
@@ -142,7 +145,14 @@
 
     @RequiresPermission(Manifest.permission.OBSERVE_GRANT_REVOKE_PERMISSIONS)
     public MediaRouterService(Context context) {
-        mLooper = Looper.getMainLooper();
+        if (Flags.enableMr2ServiceNonMainBgThread()) {
+            HandlerThread handlerThread = new HandlerThread(WORKER_THREAD_NAME);
+            handlerThread.start();
+            mLooper = handlerThread.getLooper();
+        } else {
+            mLooper = Looper.myLooper();
+        }
+        mHandler = new Handler(mLooper);
         mService2 = new MediaRouter2ServiceImpl(context, mLooper);
         mContext = context;
         Watchdog.getInstance().addMonitor(this);
diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
index 802acba..8df38a8 100644
--- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
+++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
@@ -653,9 +653,11 @@
                 return;
             }
 
-            // TODO: b/310145678 - Post this to mHandler once mHandler does not run on the main
-            // thread.
-            updateVolume();
+            if (Flags.enableMr2ServiceNonMainBgThread()) {
+                mHandler.post(SystemMediaRoute2Provider.this::updateVolume);
+            } else {
+                updateVolume();
+            }
         }
     }
 }
diff --git a/services/core/java/com/android/server/media/TEST_MAPPING b/services/core/java/com/android/server/media/TEST_MAPPING
index b3e5b9e..43e2afd 100644
--- a/services/core/java/com/android/server/media/TEST_MAPPING
+++ b/services/core/java/com/android/server/media/TEST_MAPPING
@@ -2,9 +2,7 @@
   "presubmit": [
     {
       "name": "CtsMediaBetterTogetherTestCases"
-    }
-  ],
-  "postsubmit": [
+    },
     {
       "name": "MediaRouterServiceTests"
     }
diff --git a/services/core/java/com/android/server/net/Android.bp b/services/core/java/com/android/server/net/Android.bp
index 71d8e6b..3ac2d23 100644
--- a/services/core/java/com/android/server/net/Android.bp
+++ b/services/core/java/com/android/server/net/Android.bp
@@ -1,6 +1,7 @@
 aconfig_declarations {
     name: "net_flags",
     package: "com.android.server.net",
+    container: "system",
     srcs: ["*.aconfig"],
 }
 
diff --git a/services/core/java/com/android/server/net/flags.aconfig b/services/core/java/com/android/server/net/flags.aconfig
index 419665a..d9491de 100644
--- a/services/core/java/com/android/server/net/flags.aconfig
+++ b/services/core/java/com/android/server/net/flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.net"
+container: "system"
 
 flag {
     name: "network_blocked_for_top_sleeping_and_above"
diff --git a/services/core/java/com/android/server/notification/Android.bp b/services/core/java/com/android/server/notification/Android.bp
index 9be4358..d757470 100644
--- a/services/core/java/com/android/server/notification/Android.bp
+++ b/services/core/java/com/android/server/notification/Android.bp
@@ -10,6 +10,7 @@
 aconfig_declarations {
     name: "notification_flags",
     package: "com.android.server.notification",
+    container: "system",
     srcs: [
         "flags.aconfig",
     ],
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index d4641c4..077ed5a 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.notification"
+container: "system"
 
 flag {
   name: "expire_bitmaps"
diff --git a/services/core/java/com/android/server/os/Android.bp b/services/core/java/com/android/server/os/Android.bp
index 565dc3b..c588670 100644
--- a/services/core/java/com/android/server/os/Android.bp
+++ b/services/core/java/com/android/server/os/Android.bp
@@ -1,6 +1,7 @@
 aconfig_declarations {
     name: "core_os_flags",
     package: "com.android.server.os",
+    container: "system",
     srcs: [
         "*.aconfig",
     ],
diff --git a/services/core/java/com/android/server/os/core_os_flags.aconfig b/services/core/java/com/android/server/os/core_os_flags.aconfig
index cbc0d54..ae33df8 100644
--- a/services/core/java/com/android/server/os/core_os_flags.aconfig
+++ b/services/core/java/com/android/server/os/core_os_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.os"
+container: "system"
 
 flag {
     name: "proto_tombstone"
diff --git a/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java b/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java
index 017cf55..306f77d 100644
--- a/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java
+++ b/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java
@@ -25,6 +25,7 @@
 
 import android.content.Intent;
 import android.hardware.usb.UsbManager;
+import android.nfc.NfcAdapter;
 import android.provider.AlarmClock;
 import android.provider.MediaStore;
 
@@ -361,6 +362,7 @@
                     .addCategory(Intent.CATEGORY_DEFAULT)
                     .build();
 
+
     public static List<DefaultCrossProfileIntentFilter> getDefaultManagedProfileFilters() {
         List<DefaultCrossProfileIntentFilter> filters =
                 new ArrayList<DefaultCrossProfileIntentFilter>();
@@ -637,6 +639,33 @@
                     .addDataType("video/*")
                     .build();
 
+    /** NFC TAG intent is always resolved by the primary user. */
+    static final DefaultCrossProfileIntentFilter PARENT_TO_CLONE_NFC_TAG_DISCOVERED =
+            new DefaultCrossProfileIntentFilter.Builder(
+                    DefaultCrossProfileIntentFilter.Direction.TO_PROFILE,
+                    /* flags= */0,
+                    /* letsPersonalDataIntoProfile= */ false)
+                    .addAction(NfcAdapter.ACTION_TAG_DISCOVERED)
+                    .build();
+
+    /** NFC TAG intent is always resolved by the primary user. */
+    static final DefaultCrossProfileIntentFilter PARENT_TO_CLONE_NFC_TECH_DISCOVERED =
+            new DefaultCrossProfileIntentFilter.Builder(
+                    DefaultCrossProfileIntentFilter.Direction.TO_PROFILE,
+                    /* flags= */0,
+                    /* letsPersonalDataIntoProfile= */ false)
+                    .addAction(NfcAdapter.ACTION_TECH_DISCOVERED)
+                    .build();
+
+    /** NFC TAG intent is always resolved by the primary user. */
+    static final DefaultCrossProfileIntentFilter PARENT_TO_CLONE_NFC_NDEF_DISCOVERED =
+            new DefaultCrossProfileIntentFilter.Builder(
+                    DefaultCrossProfileIntentFilter.Direction.TO_PROFILE,
+                    /* flags= */0,
+                    /* letsPersonalDataIntoProfile= */ false)
+                    .addAction(NfcAdapter.ACTION_NDEF_DISCOVERED)
+                    .build();
+
     public static List<DefaultCrossProfileIntentFilter> getDefaultCloneProfileFilters() {
         return Arrays.asList(
                 PARENT_TO_CLONE_SEND_ACTION,
@@ -652,7 +681,10 @@
                 CLONE_TO_PARENT_SMS_MMS,
                 CLONE_TO_PARENT_PHOTOPICKER_SELECTION,
                 CLONE_TO_PARENT_ACTION_PICK_IMAGES,
-                CLONE_TO_PARENT_ACTION_PICK_IMAGES_WITH_DATA_TYPES
+                CLONE_TO_PARENT_ACTION_PICK_IMAGES_WITH_DATA_TYPES,
+                PARENT_TO_CLONE_NFC_TAG_DISCOVERED,
+                PARENT_TO_CLONE_NFC_TECH_DISCOVERED,
+                PARENT_TO_CLONE_NFC_NDEF_DISCOVERED
         );
     }
 
diff --git a/services/core/java/com/android/server/policy/Android.bp b/services/core/java/com/android/server/policy/Android.bp
index fa55bf0..325b6cb 100644
--- a/services/core/java/com/android/server/policy/Android.bp
+++ b/services/core/java/com/android/server/policy/Android.bp
@@ -1,6 +1,7 @@
 aconfig_declarations {
     name: "policy_flags",
     package: "com.android.server.policy",
+    container: "system",
     srcs: ["*.aconfig"],
 }
 
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 76bf8fd..7db83d7 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -5664,6 +5664,13 @@
         }
     }
 
+    @Override
+    public void onDisplaySwitchStart(int displayId) {
+        if (displayId == DEFAULT_DISPLAY) {
+            mDefaultDisplayPolicy.onDisplaySwitchStart();
+        }
+    }
+
     private long getKeyguardDrawnTimeout() {
         final boolean bootCompleted =
                 LocalServices.getService(SystemServiceManager.class).isBootCompleted();
diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
index 5956594..2623025 100644
--- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java
+++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
@@ -895,6 +895,9 @@
         void onScreenOff();
     }
 
+    /** Called when the physical display starts to switch, e.g. fold/unfold. */
+    void onDisplaySwitchStart(int displayId);
+
     /**
      * Return whether the default display is on and not blocked by a black surface.
      */
diff --git a/services/core/java/com/android/server/policy/window_policy_flags.aconfig b/services/core/java/com/android/server/policy/window_policy_flags.aconfig
index 2154a26..7914b94 100644
--- a/services/core/java/com/android/server/policy/window_policy_flags.aconfig
+++ b/services/core/java/com/android/server/policy/window_policy_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.policy"
+container: "system"
 
 flag {
     name: "support_input_wakeup_delegate"
diff --git a/services/core/java/com/android/server/power/Android.bp b/services/core/java/com/android/server/power/Android.bp
index 863ff76..5d4065d 100644
--- a/services/core/java/com/android/server/power/Android.bp
+++ b/services/core/java/com/android/server/power/Android.bp
@@ -1,6 +1,7 @@
 aconfig_declarations {
     name: "backstage_power_flags",
     package: "com.android.server.power.optimization",
+    container: "system",
     srcs: [
         "stats/*.aconfig",
     ],
diff --git a/services/core/java/com/android/server/power/batterysaver/OWNERS b/services/core/java/com/android/server/power/batterysaver/OWNERS
index cf23bea..dc2d0b3 100644
--- a/services/core/java/com/android/server/power/batterysaver/OWNERS
+++ b/services/core/java/com/android/server/power/batterysaver/OWNERS
@@ -1,3 +1,2 @@
-kwekua@google.com
 omakoto@google.com
-yamasani@google.com
\ No newline at end of file
+yamasani@google.com
diff --git a/services/core/java/com/android/server/power/feature/Android.bp b/services/core/java/com/android/server/power/feature/Android.bp
index 2295b41..fee3114 100644
--- a/services/core/java/com/android/server/power/feature/Android.bp
+++ b/services/core/java/com/android/server/power/feature/Android.bp
@@ -1,6 +1,7 @@
 aconfig_declarations {
     name: "power_flags",
     package: "com.android.server.power.feature.flags",
+    container: "system",
     srcs: [
         "*.aconfig",
     ],
diff --git a/services/core/java/com/android/server/power/feature/power_flags.aconfig b/services/core/java/com/android/server/power/feature/power_flags.aconfig
index f5dfb5c..ca58153 100644
--- a/services/core/java/com/android/server/power/feature/power_flags.aconfig
+++ b/services/core/java/com/android/server/power/feature/power_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.power.feature.flags"
+container: "system"
 
 # Important: Flags must be accessed through PowerManagerFlags.
 
diff --git a/services/core/java/com/android/server/power/hint/Android.bp b/services/core/java/com/android/server/power/hint/Android.bp
index 8a98de6..d7dd902 100644
--- a/services/core/java/com/android/server/power/hint/Android.bp
+++ b/services/core/java/com/android/server/power/hint/Android.bp
@@ -1,6 +1,7 @@
 aconfig_declarations {
     name: "power_hint_flags",
     package: "com.android.server.power.hint",
+    container: "system",
     srcs: [
         "flags.aconfig",
     ],
diff --git a/services/core/java/com/android/server/power/hint/flags.aconfig b/services/core/java/com/android/server/power/hint/flags.aconfig
index f4afcb1..0997744 100644
--- a/services/core/java/com/android/server/power/hint/flags.aconfig
+++ b/services/core/java/com/android/server/power/hint/flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.power.hint"
+container: "system"
 
 flag {
     name: "powerhint_thread_cleanup"
diff --git a/services/core/java/com/android/server/power/stats/flags.aconfig b/services/core/java/com/android/server/power/stats/flags.aconfig
index c42ccea..54ae84f4 100644
--- a/services/core/java/com/android/server/power/stats/flags.aconfig
+++ b/services/core/java/com/android/server/power/stats/flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.power.optimization"
+container: "system"
 
 flag {
     name: "power_monitor_api"
diff --git a/services/core/java/com/android/server/stats/Android.bp b/services/core/java/com/android/server/stats/Android.bp
index e597c3a..f7955e8 100644
--- a/services/core/java/com/android/server/stats/Android.bp
+++ b/services/core/java/com/android/server/stats/Android.bp
@@ -1,6 +1,7 @@
 aconfig_declarations {
     name: "stats_flags",
     package: "com.android.server.stats",
+    container: "system",
     srcs: [
         "stats_flags.aconfig",
     ],
diff --git a/services/core/java/com/android/server/stats/stats_flags.aconfig b/services/core/java/com/android/server/stats/stats_flags.aconfig
index 5101a69..101b98e 100644
--- a/services/core/java/com/android/server/stats/stats_flags.aconfig
+++ b/services/core/java/com/android/server/stats/stats_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.stats"
+container: "system"
 
 flag {
     name: "add_mobile_bytes_transfer_by_proc_state_puller"
diff --git a/services/core/java/com/android/server/utils/Android.bp b/services/core/java/com/android/server/utils/Android.bp
index 3a334be..ffb9aec 100644
--- a/services/core/java/com/android/server/utils/Android.bp
+++ b/services/core/java/com/android/server/utils/Android.bp
@@ -1,6 +1,7 @@
 aconfig_declarations {
     name: "com.android.server.utils-aconfig",
     package: "com.android.server.utils",
+    container: "system",
     srcs: ["*.aconfig"],
 }
 
diff --git a/services/core/java/com/android/server/utils/flags.aconfig b/services/core/java/com/android/server/utils/flags.aconfig
index 163116b..6f37837 100644
--- a/services/core/java/com/android/server/utils/flags.aconfig
+++ b/services/core/java/com/android/server/utils/flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.utils"
+container: "system"
 
 flag {
      name: "anr_timer_service"
diff --git a/services/core/java/com/android/server/utils/quota/OWNERS b/services/core/java/com/android/server/utils/quota/OWNERS
index a2943f3..469acb2 100644
--- a/services/core/java/com/android/server/utils/quota/OWNERS
+++ b/services/core/java/com/android/server/utils/quota/OWNERS
@@ -1,4 +1,3 @@
 dplotnikov@google.com
-kwekua@google.com
 omakoto@google.com
 yamasani@google.com
diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
index a29cb60..ca5f26a 100644
--- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
+++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
@@ -26,6 +26,10 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.graphics.Rect;
+import android.os.Message;
+import android.os.Trace;
+import android.util.Log;
+import android.util.Slog;
 import android.view.DisplayInfo;
 import android.window.DisplayAreaInfo;
 import android.window.TransitionRequestInfo;
@@ -35,6 +39,7 @@
 import com.android.internal.display.BrightnessSynchronizer;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.server.wm.utils.DisplayInfoOverrides.DisplayInfoFieldsUpdater;
+import com.android.window.flags.Flags;
 
 import java.util.Arrays;
 import java.util.Objects;
@@ -65,6 +70,12 @@
         WM_OVERRIDE_FIELDS.setFields(out, override);
     };
 
+    private static final String TAG = "DeferredDisplayUpdater";
+
+    private static final String TRACE_TAG_WAIT_FOR_TRANSITION =
+            "Screen unblock: wait for transition";
+    private static final int WAIT_FOR_TRANSITION_TIMEOUT = 1000;
+
     private final DisplayContent mDisplayContent;
 
     @NonNull
@@ -88,6 +99,18 @@
     @NonNull
     private final DisplayInfo mOutputDisplayInfo = new DisplayInfo();
 
+    /** Whether {@link #mScreenUnblocker} should wait for transition to be ready. */
+    private boolean mShouldWaitForTransitionWhenScreenOn;
+
+    /** The message to notify PhoneWindowManager#finishWindowsDrawn. */
+    @Nullable
+    private Message mScreenUnblocker;
+
+    private final Runnable mScreenUnblockTimeoutRunnable = () -> {
+        Slog.e(TAG, "Timeout waiting for the display switch transition to start");
+        continueScreenUnblocking();
+    };
+
     public DeferredDisplayUpdater(@NonNull DisplayContent displayContent) {
         mDisplayContent = displayContent;
         mNonOverrideDisplayInfo.copyFrom(mDisplayContent.getDisplayInfo());
@@ -248,6 +271,7 @@
                 getCurrentDisplayChange(fromRotation, startBounds);
         displayChange.setPhysicalDisplayChanged(true);
 
+        transition.addTransactionCompletedListener(this::continueScreenUnblocking);
         mDisplayContent.mTransitionController.requestStartTransition(transition,
                 /* startTask= */ null, /* remoteTransition= */ null, displayChange);
 
@@ -277,6 +301,58 @@
         return !Objects.equals(first.uniqueId, second.uniqueId);
     }
 
+    @Override
+    public void onDisplayContentDisplayPropertiesPostChanged(int previousRotation, int newRotation,
+            DisplayAreaInfo newDisplayAreaInfo) {
+        // Unblock immediately in case there is no transition. This is unlikely to happen.
+        if (mScreenUnblocker != null && !mDisplayContent.mTransitionController.inTransition()) {
+            mScreenUnblocker.sendToTarget();
+            mScreenUnblocker = null;
+        }
+    }
+
+    @Override
+    public void onDisplaySwitching(boolean switching) {
+        mShouldWaitForTransitionWhenScreenOn = switching;
+    }
+
+    @Override
+    public boolean waitForTransition(@NonNull Message screenUnblocker) {
+        if (!Flags.waitForTransitionOnDisplaySwitch()) return false;
+        if (!mShouldWaitForTransitionWhenScreenOn) {
+            return false;
+        }
+        mScreenUnblocker = screenUnblocker;
+        if (Trace.isTagEnabled(Trace.TRACE_TAG_WINDOW_MANAGER)) {
+            Trace.beginAsyncSection(TRACE_TAG_WAIT_FOR_TRANSITION, screenUnblocker.hashCode());
+        }
+
+        mDisplayContent.mWmService.mH.removeCallbacks(mScreenUnblockTimeoutRunnable);
+        mDisplayContent.mWmService.mH.postDelayed(mScreenUnblockTimeoutRunnable,
+                WAIT_FOR_TRANSITION_TIMEOUT);
+        return true;
+    }
+
+    /**
+     * Continues the screen unblocking flow, could be called either on a binder thread as
+     * a result of surface transaction completed listener or from {@link WindowManagerService#mH}
+     * handler in case of timeout
+     */
+    private void continueScreenUnblocking() {
+        synchronized (mDisplayContent.mWmService.mGlobalLock) {
+            mShouldWaitForTransitionWhenScreenOn = false;
+            mDisplayContent.mWmService.mH.removeCallbacks(mScreenUnblockTimeoutRunnable);
+            if (mScreenUnblocker == null) {
+                return;
+            }
+            mScreenUnblocker.sendToTarget();
+            if (Trace.isTagEnabled(Trace.TRACE_TAG_WINDOW_MANAGER)) {
+                Trace.endAsyncSection(TRACE_TAG_WAIT_FOR_TRANSITION, mScreenUnblocker.hashCode());
+            }
+            mScreenUnblocker = null;
+        }
+    }
+
     /**
      * Diff result: fields are the same
      */
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 54abbc3..cde3e68 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -470,7 +470,7 @@
     private final DisplayRotation mDisplayRotation;
     @Nullable final DisplayRotationCompatPolicy mDisplayRotationCompatPolicy;
     DisplayFrames mDisplayFrames;
-    private final DisplayUpdater mDisplayUpdater;
+    final DisplayUpdater mDisplayUpdater;
 
     private boolean mInTouchMode;
 
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 16f7373..a5037ea 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -779,6 +779,11 @@
         return mLidState;
     }
 
+    private void onDisplaySwitchFinished() {
+        mDisplayContent.mWallpaperController.onDisplaySwitchFinished();
+        mDisplayContent.mDisplayUpdater.onDisplaySwitching(false);
+    }
+
     public void setAwake(boolean awake) {
         synchronized (mLock) {
             if (awake == mAwake) {
@@ -797,7 +802,7 @@
             mService.mAtmService.mKeyguardController.updateDeferTransitionForAod(
                     mAwake /* waiting */);
             if (!awake) {
-                mDisplayContent.mWallpaperController.onDisplaySwitchFinished();
+                onDisplaySwitchFinished();
             }
         }
     }
@@ -866,7 +871,7 @@
 
     /** It is called after {@link #finishScreenTurningOn}. This runs on PowerManager's thread. */
     public void screenTurnedOn() {
-        mDisplayContent.mWallpaperController.onDisplaySwitchFinished();
+        onDisplaySwitchFinished();
     }
 
     public void screenTurnedOff() {
@@ -2187,6 +2192,11 @@
                 mDisplayContent.mTransitionController.getCollectingTransitionId();
     }
 
+    /** If this is called, expect that there will be an onDisplayChanged about unique id. */
+    public void onDisplaySwitchStart() {
+        mDisplayContent.mDisplayUpdater.onDisplaySwitching(true);
+    }
+
     @NavigationBarPosition
     int navigationBarPosition(int displayRotation) {
         if (mNavigationBar != null) {
diff --git a/services/core/java/com/android/server/wm/DisplayUpdater.java b/services/core/java/com/android/server/wm/DisplayUpdater.java
index e611177..918b180 100644
--- a/services/core/java/com/android/server/wm/DisplayUpdater.java
+++ b/services/core/java/com/android/server/wm/DisplayUpdater.java
@@ -17,6 +17,7 @@
 package com.android.server.wm;
 
 import android.annotation.NonNull;
+import android.os.Message;
 import android.view.Surface;
 import android.window.DisplayAreaInfo;
 
@@ -49,4 +50,16 @@
             @Surface.Rotation int previousRotation, @Surface.Rotation int newRotation,
             @NonNull DisplayAreaInfo newDisplayAreaInfo) {
     }
+
+    /**
+     * Called with {@code true} when physical display is going to switch. And {@code false} when
+     * the display is turned on or the device goes to sleep.
+     */
+    default void onDisplaySwitching(boolean switching) {
+    }
+
+    /** Returns {@code true} if the transition will control when to turn on the screen. */
+    default boolean waitForTransition(@NonNull Message screenUnBlocker) {
+        return false;
+    }
 }
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 3779d9e..1b380aa 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -112,6 +112,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.Executor;
 import java.util.function.Predicate;
 
 /**
@@ -233,6 +234,9 @@
      */
     private ArrayList<Task> mTransientHideTasks;
 
+    @VisibleForTesting
+    ArrayList<Runnable> mTransactionCompletedListeners = null;
+
     /** Custom activity-level animation options and callbacks. */
     private TransitionInfo.AnimationOptions mOverrideOptions;
     private IRemoteCallback mClientAnimationStartCallback = null;
@@ -1640,6 +1644,14 @@
         commitVisibleActivities(transaction);
         commitVisibleWallpapers();
 
+        if (mTransactionCompletedListeners != null) {
+            for (int i = 0; i < mTransactionCompletedListeners.size(); i++) {
+                final Runnable listener = mTransactionCompletedListeners.get(i);
+                transaction.addTransactionCompletedListener(Runnable::run,
+                        (stats) -> listener.run());
+            }
+        }
+
         // Fall-back to the default display if there isn't one participating.
         final DisplayContent primaryDisplay = !mTargetDisplays.isEmpty() ? mTargetDisplays.get(0)
                 : mController.mAtm.mRootWindowContainer.getDefaultDisplay();
@@ -1862,6 +1874,17 @@
     }
 
     /**
+     * Adds a listener that will be executed after the start transaction of this transition
+     * is presented on the screen, the listener will be executed on a binder thread
+     */
+    void addTransactionCompletedListener(Runnable listener) {
+        if (mTransactionCompletedListeners == null) {
+            mTransactionCompletedListeners = new ArrayList<>();
+        }
+        mTransactionCompletedListeners.add(listener);
+    }
+
+    /**
      * Checks if the transition contains order changes.
      *
      * This is a shallow check that doesn't account for collection in parallel, unlike
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 04ca0ae..2e72121 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -8077,6 +8077,10 @@
             }
             boolean allWindowsDrawn = false;
             synchronized (mGlobalLock) {
+                if (mRoot.getDefaultDisplay().mDisplayUpdater.waitForTransition(message)) {
+                    // Use the ready-to-play of transition as the signal.
+                    return;
+                }
                 container.waitForAllWindowsDrawn();
                 mWindowPlacerLocked.requestTraversal();
                 mH.removeMessages(H.WAITING_FOR_DRAWN_TIMEOUT, container);
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/BundlePolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/BundlePolicySerializer.java
index 82f9aad..d24afabe 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/BundlePolicySerializer.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/BundlePolicySerializer.java
@@ -92,7 +92,7 @@
                 while (count > 0 && (type = parser.next()) != XmlPullParser.END_DOCUMENT) {
                     if (type == XmlPullParser.START_TAG
                             && parser.getName().equals(TAG_VALUE)) {
-                        values.add(parser.nextText().trim());
+                        values.add(parser.nextText());
                         count--;
                     }
                 }
@@ -111,7 +111,7 @@
                 restrictions.putParcelableArray(key,
                         bundleList.toArray(new Bundle[bundleList.size()]));
             } else {
-                String value = parser.nextText().trim();
+                String value = parser.nextText();
                 if (ATTR_TYPE_BOOLEAN.equals(valType)) {
                     restrictions.putBoolean(key, Boolean.parseBoolean(value));
                 } else if (ATTR_TYPE_INTEGER.equals(valType)) {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index b34092c..3dd7b54 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -11509,10 +11509,17 @@
 
     @Override
     public void setApplicationRestrictions(ComponentName who, String callerPackage,
-            String packageName, Bundle restrictions) {
+            String packageName, Bundle restrictions, boolean parent) {
         final CallerIdentity caller = getCallerIdentity(who, callerPackage);
         checkCanExecuteOrThrowUnsafe(DevicePolicyManager.OPERATION_SET_APPLICATION_RESTRICTIONS);
 
+        // This check is eventually made in UMS, checking here to fail early.
+        String validationResult =
+                FrameworkParsingPackageUtils.validateName(packageName, false, false);
+        if (validationResult != null) {
+            throw new IllegalArgumentException("Invalid package name: " + validationResult);
+        }
+
         if (isUnicornFlagEnabled()) {
             EnforcingAdmin enforcingAdmin = enforcePermissionAndGetEnforcingAdmin(
                     who,
@@ -11520,12 +11527,6 @@
                     caller.getPackageName(),
                     caller.getUserId()
             );
-            // This check is eventually made in UMS, checking here to fail early.
-            String validationResult =
-                    FrameworkParsingPackageUtils.validateName(packageName, false, false);
-            if (validationResult != null) {
-                throw new IllegalArgumentException("Invalid package name: " + validationResult);
-            }
 
             if (restrictions == null || restrictions.isEmpty()) {
                 mDevicePolicyEngine.removeLocalPolicy(
@@ -11541,6 +11542,57 @@
             }
             setBackwardsCompatibleAppRestrictions(
                     caller, packageName, restrictions, caller.getUserHandle());
+        } else if (Flags.dmrhCanSetAppRestriction()) {
+            final boolean isRoleHolder;
+            if (who != null) {
+                // DO or PO
+                Preconditions.checkCallAuthorization(
+                        (isProfileOwner(caller) || isDefaultDeviceOwner(caller)));
+                Preconditions.checkCallAuthorization(!parent,
+                        "DO or PO cannot call this on parent");
+                // Caller has opted to be treated as DPC (by passing a non-null who), so don't
+                // consider it as the DMRH, even if the caller is both the DPC and the DMRH.
+                isRoleHolder = false;
+            } else {
+                // Delegates, or the DMRH. Only DMRH can call this on COPE parent
+                isRoleHolder = isCallerDevicePolicyManagementRoleHolder(caller);
+                if (parent) {
+                    Preconditions.checkCallAuthorization(isRoleHolder);
+                    Preconditions.checkState(isOrganizationOwnedDeviceWithManagedProfile(),
+                            "Role Holder can only operate parent app restriction on COPE devices");
+                } else {
+                    Preconditions.checkCallAuthorization(isRoleHolder
+                            || isCallerDelegate(caller, DELEGATION_APP_RESTRICTIONS));
+                }
+            }
+            // DMRH caller uses policy engine, others still use legacy code path
+            if (isRoleHolder) {
+                EnforcingAdmin enforcingAdmin = getEnforcingAdminForCaller(/* who */ null,
+                        caller.getPackageName());
+                int affectedUserId = parent
+                        ? getProfileParentId(caller.getUserId()) : caller.getUserId();
+                if (restrictions == null || restrictions.isEmpty()) {
+                    mDevicePolicyEngine.removeLocalPolicy(
+                            PolicyDefinition.APPLICATION_RESTRICTIONS(packageName),
+                            enforcingAdmin,
+                            affectedUserId);
+                } else {
+                    mDevicePolicyEngine.setLocalPolicy(
+                            PolicyDefinition.APPLICATION_RESTRICTIONS(packageName),
+                            enforcingAdmin,
+                            new BundlePolicyValue(restrictions),
+                            affectedUserId);
+                }
+                Intent changeIntent = new Intent(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED);
+                changeIntent.setPackage(packageName);
+                changeIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+                mContext.sendBroadcastAsUser(changeIntent, UserHandle.of(affectedUserId));
+            } else {
+                mInjector.binderWithCleanCallingIdentity(() -> {
+                    mUserManager.setApplicationRestrictions(packageName, restrictions,
+                            caller.getUserHandle());
+                });
+            }
         } else {
             Preconditions.checkCallAuthorization((caller.hasAdminComponent()
                     && (isProfileOwner(caller) || isDefaultDeviceOwner(caller)))
@@ -12872,7 +12924,7 @@
 
     @Override
     public Bundle getApplicationRestrictions(ComponentName who, String callerPackage,
-            String packageName) {
+            String packageName, boolean parent) {
         final CallerIdentity caller = getCallerIdentity(who, callerPackage);
 
         if (isUnicornFlagEnabled()) {
@@ -12891,6 +12943,50 @@
                 return Bundle.EMPTY;
             }
             return policies.get(enforcingAdmin).getValue();
+        } else if (Flags.dmrhCanSetAppRestriction()) {
+            final boolean isRoleHolder;
+            if (who != null) {
+                // Caller is DO or PO. They cannot call this on parent
+                Preconditions.checkCallAuthorization(!parent
+                        && (isProfileOwner(caller) || isDefaultDeviceOwner(caller)));
+                // Caller has opted to be treated as DPC (by passing a non-null who), so don't
+                // consider it as the DMRH, even if the caller is both the DPC and the DMRH.
+                isRoleHolder = false;
+            } else {
+                // Caller is delegates or the DMRH. Only DMRH can call this on parent
+                isRoleHolder = isCallerDevicePolicyManagementRoleHolder(caller);
+                if (parent) {
+                    Preconditions.checkCallAuthorization(isRoleHolder);
+                    Preconditions.checkState(isOrganizationOwnedDeviceWithManagedProfile(),
+                            "Role Holder can only operate parent app restriction on COPE devices");
+                } else {
+                    Preconditions.checkCallAuthorization(isRoleHolder
+                            || isCallerDelegate(caller, DELEGATION_APP_RESTRICTIONS));
+                }
+            }
+            if (isRoleHolder) {
+                EnforcingAdmin enforcingAdmin = getEnforcingAdminForCaller(/* who */ null,
+                        caller.getPackageName());
+                int affectedUserId = parent
+                        ? getProfileParentId(caller.getUserId()) : caller.getUserId();
+                LinkedHashMap<EnforcingAdmin, PolicyValue<Bundle>> policies =
+                        mDevicePolicyEngine.getLocalPoliciesSetByAdmins(
+                                PolicyDefinition.APPLICATION_RESTRICTIONS(packageName),
+                                affectedUserId);
+                if (!policies.containsKey(enforcingAdmin)) {
+                    return Bundle.EMPTY;
+                }
+                return policies.get(enforcingAdmin).getValue();
+            } else {
+                return mInjector.binderWithCleanCallingIdentity(() -> {
+                    Bundle bundle = mUserManager.getApplicationRestrictions(packageName,
+                            caller.getUserHandle());
+                    // if no restrictions were saved, mUserManager.getApplicationRestrictions
+                    // returns null, but DPM method should return an empty Bundle as per JavaDoc
+                    return bundle != null ? bundle : Bundle.EMPTY;
+                });
+            }
+
         } else {
             Preconditions.checkCallAuthorization((caller.hasAdminComponent()
                     && (isProfileOwner(caller) || isDefaultDeviceOwner(caller)))
@@ -15811,19 +15907,16 @@
             for (EnforcingAdmin admin : policies.keySet()) {
                 restrictions.add(policies.get(admin).getValue());
             }
-            if (!restrictions.isEmpty()) {
-                return restrictions;
-            }
 
             return mInjector.binderWithCleanCallingIdentity(() -> {
-                // Could be a device that has a DPC that hasn't migrated yet, so just return any
+                // Could be a device that has a DPC that hasn't migrated yet, so also return any
                 // restrictions saved in userManager.
                 Bundle bundle = mUserManager.getApplicationRestrictions(
                         packageName, UserHandle.of(userId));
-                if (bundle == null || bundle.isEmpty()) {
-                    return new ArrayList<>();
+                if (bundle != null && !bundle.isEmpty()) {
+                    restrictions.add(bundle);
                 }
-                return List.of(bundle);
+                return restrictions;
             });
         }
 
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/feature/Android.bp b/services/foldables/devicestateprovider/src/com/android/server/policy/feature/Android.bp
index 6ad8d79..6393e11 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/feature/Android.bp
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/feature/Android.bp
@@ -1,6 +1,7 @@
 aconfig_declarations {
     name: "device_state_flags",
     package: "com.android.server.policy.feature.flags",
+    container: "system",
     srcs: [
         "device_state_flags.aconfig",
     ],
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/feature/device_state_flags.aconfig b/services/foldables/devicestateprovider/src/com/android/server/policy/feature/device_state_flags.aconfig
index 29e258c..21e33dd 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/feature/device_state_flags.aconfig
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/feature/device_state_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.policy.feature.flags"
+container: "system"
 
 flag {
     name: "enable_dual_display_blocking"
diff --git a/services/permission/TEST_MAPPING b/services/permission/TEST_MAPPING
index 00bfcd3..4de4a56 100644
--- a/services/permission/TEST_MAPPING
+++ b/services/permission/TEST_MAPPING
@@ -103,6 +103,28 @@
                     "include-filter": "android.permission.cts.PermissionMaxSdkVersionTest"
                 }
             ]
+        },
+        {
+            "name": "CtsVirtualDevicesAudioTestCases",
+            "options": [
+                {
+                    "exclude-annotation": "androidx.test.filters.FlakyTest"
+                },
+                {
+                    "include-filter": "android.virtualdevice.cts.audio.VirtualAudioPermissionTest"
+                }
+            ]
+        },
+        {
+            "name": "CtsVirtualDevicesAppLaunchTestCases",
+            "options": [
+                {
+                    "exclude-annotation": "androidx.test.filters.FlakyTest"
+                },
+                {
+                    "include-filter": "android.virtualdevice.cts.applaunch.VirtualDevicePermissionTest"
+                }
+            ]
         }
     ],
     "imports": [
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
index 5a50510..1a03e78 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
@@ -79,12 +79,16 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.foldables.FoldGracePeriodProvider;
+import com.android.internal.util.test.LocalServiceKeeperRule;
+import com.android.server.LocalServices;
 import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.layout.DisplayIdProducer;
 import com.android.server.display.layout.Layout;
+import com.android.server.policy.WindowManagerPolicy;
 import com.android.server.utils.FoldSettingProvider;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -124,6 +128,9 @@
 
     private DeviceStateToLayoutMap mDeviceStateToLayoutMapSpy;
 
+    @Rule
+    public LocalServiceKeeperRule mLocalServiceKeeperRule = new LocalServiceKeeperRule();
+
     @Mock LogicalDisplayMapper.Listener mListenerMock;
     @Mock Context mContextMock;
     @Mock FoldSettingProvider mFoldSettingProviderMock;
@@ -133,6 +140,7 @@
     @Mock IThermalService mIThermalServiceMock;
     @Mock DisplayManagerFlags mFlagsMock;
     @Mock DisplayAdapter mDisplayAdapterMock;
+    @Mock WindowManagerPolicy mWindowManagerPolicy;
 
     @Captor ArgumentCaptor<LogicalDisplay> mDisplayCaptor;
     @Captor ArgumentCaptor<Integer> mDisplayEventCaptor;
@@ -143,6 +151,9 @@
         System.setProperty("dexmaker.share_classloader", "true");
         MockitoAnnotations.initMocks(this);
 
+        mLocalServiceKeeperRule.overrideLocalService(WindowManagerPolicy.class,
+                mWindowManagerPolicy);
+
         mDeviceStateToLayoutMapSpy =
                 spy(new DeviceStateToLayoutMap(mIdProducer, mFlagsMock, NON_EXISTING_FILE));
         mDisplayDeviceRepo = new DisplayDeviceRepository(
@@ -194,6 +205,7 @@
                 mDisplayDeviceRepo,
                 mListenerMock, new DisplayManagerService.SyncRoot(), mHandler,
                 mDeviceStateToLayoutMapSpy, mFlagsMock);
+        mLogicalDisplayMapper.onWindowManagerReady();
     }
 
 
@@ -757,6 +769,44 @@
     }
 
     @Test
+    public void testDisplaySwappedAfterDeviceStateChange_windowManagerIsNotified() {
+        FoldableDisplayDevices foldableDisplayDevices = createFoldableDeviceStateToLayoutMap();
+        mLogicalDisplayMapper.setDeviceStateLocked(DEVICE_STATE_OPEN);
+        mLogicalDisplayMapper.onEarlyInteractivityChange(true);
+        mLogicalDisplayMapper.onBootCompleted();
+        advanceTime(1000);
+        clearInvocations(mWindowManagerPolicy);
+
+        // Switch from 'inner' to 'outer' display (fold a foldable device)
+        mLogicalDisplayMapper.setDeviceStateLocked(DEVICE_STATE_CLOSED);
+        // Continue folding device state transition by turning off the inner display
+        foldableDisplayDevices.mInner.setState(STATE_OFF);
+        notifyDisplayChanges(foldableDisplayDevices.mOuter);
+        advanceTime(TIMEOUT_STATE_TRANSITION_MILLIS);
+
+        verify(mWindowManagerPolicy).onDisplaySwitchStart(DEFAULT_DISPLAY);
+    }
+
+    @Test
+    public void testCreateNewLogicalDisplay_windowManagerIsNotNotifiedAboutSwitch() {
+        DisplayDevice device1 = createDisplayDevice(TYPE_EXTERNAL, 600, 800,
+                FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
+        when(mDeviceStateToLayoutMapSpy.size()).thenReturn(1);
+        LogicalDisplay display1 = add(device1);
+
+        assertTrue(display1.isEnabledLocked());
+
+        DisplayDevice device2 = createDisplayDevice(TYPE_INTERNAL, 600, 800,
+                FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY);
+        when(mDeviceStateToLayoutMapSpy.size()).thenReturn(2);
+        add(device2);
+
+        // As it is not a display switch but adding a new display, we should not notify
+        // about display switch start to window manager
+        verify(mWindowManagerPolicy, never()).onDisplaySwitchStart(anyInt());
+    }
+
+    @Test
     public void testDoNotWaitForSleepWhenFoldSettingStayAwake() {
         // Test device should be marked ready for transition immediately when 'Continue using app
         // on fold' set to 'Always'
diff --git a/services/tests/displayservicetests/src/com/android/server/display/NormalBrightnessModeControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/NormalBrightnessModeControllerTest.java
index c379d6b..3fd3cef 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/NormalBrightnessModeControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/NormalBrightnessModeControllerTest.java
@@ -43,6 +43,11 @@
 
     private final NormalBrightnessModeController mController = new NormalBrightnessModeController();
 
+    // AutoBrightnessController sends ambientLux values *only* when auto brightness enabled.
+    // NormalBrightnessModeController is temporary disabled  if auto brightness is off,
+    // to avoid capping brightness based on stale ambient lux. Temporary disabling tests with
+    // auto brightness off and default config pres
+    // The issue is tracked here: b/322445088
     @Keep
     private static Object[][] brightnessData() {
         return new Object[][]{
@@ -59,10 +64,10 @@
                         ImmutableMap.of(99f, 0.1f, 101f, 0.2f)
                 ), 0.2f},
                 // Auto brightness - off, config only for default
-                {100, AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED, ImmutableMap.of(
-                        BrightnessLimitMapType.DEFAULT,
-                        ImmutableMap.of(99f, 0.1f, 101f, 0.2f)
-                ), 0.2f},
+                // {100, AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED, ImmutableMap.of(
+                //        BrightnessLimitMapType.DEFAULT,
+                //        ImmutableMap.of(99f, 0.1f, 101f, 0.2f)
+                // ), 0.2f},
                 // Auto brightness - off, config only for adaptive
                 {100, AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED, ImmutableMap.of(
                         BrightnessLimitMapType.ADAPTIVE,
@@ -81,12 +86,12 @@
                         ImmutableMap.of(99f, 0.3f, 101f, 0.4f)
                 ), 0.4f},
                 // Auto brightness - off, config for both
-                {100, AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED, ImmutableMap.of(
-                        BrightnessLimitMapType.DEFAULT,
-                        ImmutableMap.of(99f, 0.1f, 101f, 0.2f),
-                        BrightnessLimitMapType.ADAPTIVE,
-                        ImmutableMap.of(99f, 0.3f, 101f, 0.4f)
-                ), 0.2f},
+                // {100, AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED, ImmutableMap.of(
+                //        BrightnessLimitMapType.DEFAULT,
+                //        ImmutableMap.of(99f, 0.1f, 101f, 0.2f),
+                //        BrightnessLimitMapType.ADAPTIVE,
+                //        ImmutableMap.of(99f, 0.3f, 101f, 0.4f)
+                // ), 0.2f},
                 // Auto brightness - on, config for both, ambient high
                 {1000, AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED, ImmutableMap.of(
                         BrightnessLimitMapType.DEFAULT,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java
index 87fc7a4..39ffe5b 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java
@@ -33,6 +33,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.server.display.AutomaticBrightnessController;
 import com.android.server.display.config.HdrBrightnessData;
 import com.android.server.testutils.OffsettableClock;
 import com.android.server.testutils.TestHandler;
@@ -230,6 +231,11 @@
     }
 
     private void configureClamper() {
+        // AutoBrightnessController sends ambientLux values *only* when auto brightness enabled.
+        // HdrClamper is temporary disabled  if auto brightness is off.
+        // Temporary setting AutoBrightnessState to enabled for this test
+        // The issue is tracked here: b/322445088
+        mHdrClamper.setAutoBrightnessState(AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED);
         mHdrClamper.resetHdrConfig(TEST_HDR_DATA, WIDTH, HEIGHT, MIN_HDR_PERCENT, mMockBinder);
         mHdrChangeListener.onHdrVisible(true);
     }
diff --git a/services/tests/media/mediarouterservicetest/src/com/android/server/media/AudioManagerRouteControllerTest.java b/services/tests/media/mediarouterservicetest/src/com/android/server/media/AudioManagerRouteControllerTest.java
index a918be2..8bdfc50 100644
--- a/services/tests/media/mediarouterservicetest/src/com/android/server/media/AudioManagerRouteControllerTest.java
+++ b/services/tests/media/mediarouterservicetest/src/com/android/server/media/AudioManagerRouteControllerTest.java
@@ -69,6 +69,13 @@
 public class AudioManagerRouteControllerTest {
 
     private static final String FAKE_ROUTE_NAME = "fake name";
+
+    /**
+     * The number of milliseconds to wait for an asynchronous operation before failing an associated
+     * assertion.
+     */
+    private static final int ASYNC_CALL_TIMEOUTS_MS = 1000;
+
     private static final AudioDeviceInfo FAKE_AUDIO_DEVICE_INFO_BUILTIN_SPEAKER =
             createAudioDeviceInfo(
                     AudioSystem.DEVICE_OUT_SPEAKER, "name_builtin", /* address= */ null);
@@ -231,7 +238,7 @@
         MediaRoute2Info builtInSpeakerRoute =
                 getAvailableRouteWithType(MediaRoute2Info.TYPE_BUILTIN_SPEAKER);
         mControllerUnderTest.transferTo(builtInSpeakerRoute.getId());
-        verify(mMockAudioManager)
+        verify(mMockAudioManager, Mockito.timeout(ASYNC_CALL_TIMEOUTS_MS))
                 .setPreferredDeviceForStrategy(
                         mMediaAudioProductStrategy,
                         createAudioDeviceAttribute(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER));
@@ -239,7 +246,7 @@
         MediaRoute2Info wiredHeadsetRoute =
                 getAvailableRouteWithType(MediaRoute2Info.TYPE_WIRED_HEADSET);
         mControllerUnderTest.transferTo(wiredHeadsetRoute.getId());
-        verify(mMockAudioManager)
+        verify(mMockAudioManager, Mockito.timeout(ASYNC_CALL_TIMEOUTS_MS))
                 .setPreferredDeviceForStrategy(
                         mMediaAudioProductStrategy,
                         createAudioDeviceAttribute(AudioDeviceInfo.TYPE_WIRED_HEADSET));
diff --git a/services/tests/mockingservicestests/src/com/android/server/OWNERS b/services/tests/mockingservicestests/src/com/android/server/OWNERS
index f801560..0eb8639 100644
--- a/services/tests/mockingservicestests/src/com/android/server/OWNERS
+++ b/services/tests/mockingservicestests/src/com/android/server/OWNERS
@@ -1,5 +1,5 @@
 per-file *Alarm* = file:/apex/jobscheduler/OWNERS
 per-file *AppStateTracker* = file:/apex/jobscheduler/OWNERS
 per-file *DeviceIdleController* = file:/apex/jobscheduler/OWNERS
-per-file SensitiveContentProtectionManagerServiceTest.java = file:/core/java/android/permission/OWNERS
+per-file SensitiveContentProtectionManagerService* = file:/core/java/android/permission/OWNERS
 per-file RescuePartyTest.java = file:/packages/CrashRecovery/OWNERS
diff --git a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
index 682569f..697548c 100644
--- a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
@@ -1111,16 +1111,9 @@
 
         // mock properties in BootThreshold
         try {
-            if (Flags.recoverabilityDetection()) {
-                mSpyBootThreshold = spy(watchdog.new BootThreshold(
-                    PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
-                    PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS,
-                    PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT));
-            } else {
-                mSpyBootThreshold = spy(watchdog.new BootThreshold(
+            mSpyBootThreshold = spy(watchdog.new BootThreshold(
                     PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
                     PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS));
-            }
             mCrashRecoveryPropertiesMap = new HashMap<>();
 
             doAnswer((Answer<Integer>) invocationOnMock -> {
diff --git a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java
index 5065144..edee8cd 100644
--- a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java
@@ -25,7 +25,6 @@
 import static org.mockito.Mockito.doCallRealMethod;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
@@ -34,6 +33,7 @@
 import android.content.pm.PackageManagerInternal;
 import android.media.projection.MediaProjectionInfo;
 import android.media.projection.MediaProjectionManager;
+import android.os.Process;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -79,7 +79,8 @@
     private static final String NOTIFICATION_PKG_1 = "com.android.server.notification.one";
     private static final String NOTIFICATION_PKG_2 = "com.android.server.notification.two";
 
-    private static final String EXEMPTED_SCREEN_RECORDER_PACKAGE = "test.screen.recorder.package";
+    private static final String SCREEN_RECORDER_PACKAGE = "test.screen.recorder.package";
+    private static final String EXEMPTED_SCREEN_RECORDER_PACKAGE = "exempt.screen.recorder.package";
 
     private static final int NOTIFICATION_UID_1 = 5;
     private static final int NOTIFICATION_UID_2 = 6;
@@ -281,10 +282,18 @@
                 .getActiveNotifications();
     }
 
+    private MediaProjectionInfo createMediaProjectionInfo() {
+        return new MediaProjectionInfo(SCREEN_RECORDER_PACKAGE, Process.myUserHandle(), null);
+    }
+
+    private MediaProjectionInfo createExemptMediaProjectionInfo() {
+        return new MediaProjectionInfo(
+                EXEMPTED_SCREEN_RECORDER_PACKAGE, Process.myUserHandle(), null);
+    }
+
     @Test
     public void mediaProjectionOnStart_verifyExemptedRecorderPackage() {
-        MediaProjectionInfo mediaProjectionInfo = mock(MediaProjectionInfo.class);
-        when(mediaProjectionInfo.getPackageName()).thenReturn(EXEMPTED_SCREEN_RECORDER_PACKAGE);
+        MediaProjectionInfo mediaProjectionInfo = createExemptMediaProjectionInfo();
 
         mMediaProjectionCallbackCaptor.getValue().onStart(mediaProjectionInfo);
 
@@ -295,7 +304,7 @@
     public void mediaProjectionOnStart_onProjectionStart_setWmBlockedPackages() {
         ArraySet<PackageInfo> expectedBlockedPackages = setupSensitiveNotification();
 
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
 
         verify(mWindowManager).addBlockScreenCaptureForApps(expectedBlockedPackages);
     }
@@ -304,18 +313,18 @@
     public void mediaProjectionOnStart_noSensitiveNotifications_noBlockedPackages() {
         setupNoSensitiveNotifications();
 
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
 
-        verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET);
+        verifyZeroInteractions(mWindowManager);
     }
 
     @Test
     public void mediaProjectionOnStart_noNotifications_noBlockedPackages() {
         setupNoNotifications();
 
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
 
-        verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET);
+        verifyZeroInteractions(mWindowManager);
     }
 
     @Test
@@ -323,7 +332,7 @@
         ArraySet<PackageInfo> expectedBlockedPackages =
                 setupMultipleSensitiveNotificationsFromSamePackageAndUid();
 
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
 
         verify(mWindowManager).addBlockScreenCaptureForApps(expectedBlockedPackages);
     }
@@ -333,7 +342,7 @@
         ArraySet<PackageInfo> expectedBlockedPackages =
                 setupMultipleSensitiveNotificationsFromDifferentPackage();
 
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
 
         verify(mWindowManager).addBlockScreenCaptureForApps(expectedBlockedPackages);
     }
@@ -343,7 +352,7 @@
         ArraySet<PackageInfo> expectedBlockedPackages =
                 setupMultipleSensitiveNotificationsFromDifferentUid();
 
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
 
         verify(mWindowManager).addBlockScreenCaptureForApps(expectedBlockedPackages);
     }
@@ -352,7 +361,7 @@
     public void mediaProjectionOnStop_onProjectionEnd_clearWmBlockedPackages() {
         setupSensitiveNotification();
 
-        MediaProjectionInfo mediaProjectionInfo = mock(MediaProjectionInfo.class);
+        MediaProjectionInfo mediaProjectionInfo = createMediaProjectionInfo();
         mMediaProjectionCallbackCaptor.getValue().onStart(mediaProjectionInfo);
         Mockito.reset(mWindowManager);
 
@@ -365,7 +374,7 @@
     public void mediaProjectionOnStart_afterOnStop_onProjectionStart_setWmBlockedPackages() {
         ArraySet<PackageInfo> expectedBlockedPackages = setupSensitiveNotification();
 
-        MediaProjectionInfo mediaProjectionInfo = mock(MediaProjectionInfo.class);
+        MediaProjectionInfo mediaProjectionInfo = createMediaProjectionInfo();
         mMediaProjectionCallbackCaptor.getValue().onStart(mediaProjectionInfo);
         mMediaProjectionCallbackCaptor.getValue().onStop(mediaProjectionInfo);
         Mockito.reset(mWindowManager);
@@ -381,9 +390,9 @@
                 .when(mSensitiveContentProtectionManagerService.mNotificationListener)
                 .getActiveNotifications();
 
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
 
-        verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET);
+        verifyZeroInteractions(mWindowManager);
     }
 
     @Test
@@ -392,9 +401,9 @@
                 .when(mSensitiveContentProtectionManagerService.mNotificationListener)
                 .getCurrentRanking();
 
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
 
-        verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET);
+        verifyZeroInteractions(mWindowManager);
     }
 
     @Test
@@ -403,9 +412,9 @@
                 .when(mSensitiveContentProtectionManagerService.mNotificationListener)
                 .getCurrentRanking();
 
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
 
-        verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET);
+        verifyZeroInteractions(mWindowManager);
     }
 
     @Test
@@ -416,9 +425,9 @@
                 .when(mSensitiveContentProtectionManagerService.mNotificationListener)
                 .getCurrentRanking();
 
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
 
-        verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET);
+        verifyZeroInteractions(mWindowManager);
     }
 
     @Test
@@ -426,7 +435,7 @@
         mockDisabledViaDevelopOption();
         setupSensitiveNotification();
 
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
 
         verifyZeroInteractions(mWindowManager);
     }
@@ -447,8 +456,9 @@
         // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2
         // as non-sensitive
         setupSensitiveNotification();
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
-        mMediaProjectionCallbackCaptor.getValue().onStop(mock(MediaProjectionInfo.class));
+        MediaProjectionInfo mediaProjectionInfo = createMediaProjectionInfo();
+        mMediaProjectionCallbackCaptor.getValue().onStart(mediaProjectionInfo);
+        mMediaProjectionCallbackCaptor.getValue().onStop(mediaProjectionInfo);
         Mockito.reset(mWindowManager);
 
         mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected();
@@ -461,7 +471,7 @@
         // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2
         // as non-sensitive
         ArraySet<PackageInfo> expectedBlockedPackages = setupSensitiveNotification();
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
         Mockito.reset(mWindowManager);
 
         mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected();
@@ -472,23 +482,23 @@
     @Test
     public void nlsOnListenerConnected_noSensitiveNotifications_noBlockedPackages() {
         setupNoSensitiveNotifications();
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
         Mockito.reset(mWindowManager);
 
         mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected();
 
-        verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET);
+        verifyZeroInteractions(mWindowManager);
     }
 
     @Test
     public void nlsOnListenerConnected_noNotifications_noBlockedPackages() {
         setupNoNotifications();
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
         Mockito.reset(mWindowManager);
 
         mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected();
 
-        verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET);
+        verifyZeroInteractions(mWindowManager);
     }
 
     @Test
@@ -496,7 +506,7 @@
         // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2
         // as non-sensitive
         setupSensitiveNotification();
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
         Mockito.reset(mWindowManager);
         doReturn(null)
                 .when(mSensitiveContentProtectionManagerService.mNotificationListener)
@@ -504,7 +514,7 @@
 
         mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected();
 
-        verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET);
+        verifyZeroInteractions(mWindowManager);
     }
 
     @Test
@@ -512,7 +522,7 @@
         // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2
         // as non-sensitive
         setupSensitiveNotification();
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
         Mockito.reset(mWindowManager);
         when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_1))).thenReturn(null);
         doReturn(mRankingMap)
@@ -521,7 +531,7 @@
 
         mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected();
 
-        verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET);
+        verifyZeroInteractions(mWindowManager);
     }
 
     @Test
@@ -530,7 +540,7 @@
         // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2
         // as non-sensitive
         setupSensitiveNotification();
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
         mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected();
 
         verifyZeroInteractions(mWindowManager);
@@ -553,8 +563,9 @@
         // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2
         // as non-sensitive
         setupSensitiveNotification();
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
-        mMediaProjectionCallbackCaptor.getValue().onStop(mock(MediaProjectionInfo.class));
+        MediaProjectionInfo mediaProjectionInfo = createMediaProjectionInfo();
+        mMediaProjectionCallbackCaptor.getValue().onStart(mediaProjectionInfo);
+        mMediaProjectionCallbackCaptor.getValue().onStop(mediaProjectionInfo);
         Mockito.reset(mWindowManager);
 
         mSensitiveContentProtectionManagerService.mNotificationListener
@@ -568,7 +579,7 @@
         // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2
         // as non-sensitive
         ArraySet<PackageInfo> expectedBlockedPackages = setupSensitiveNotification();
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
         Mockito.reset(mWindowManager);
 
         mSensitiveContentProtectionManagerService.mNotificationListener
@@ -580,25 +591,25 @@
     @Test
     public void nlsOnNotificationRankingUpdate_noSensitiveNotifications_noBlockedPackages() {
         setupNoSensitiveNotifications();
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
         Mockito.reset(mWindowManager);
 
         mSensitiveContentProtectionManagerService.mNotificationListener
                 .onNotificationRankingUpdate(mRankingMap);
 
-        verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET);
+        verifyZeroInteractions(mWindowManager);
     }
 
     @Test
     public void nlsOnNotificationRankingUpdate_noNotifications_noBlockedPackages() {
         setupNoNotifications();
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
         Mockito.reset(mWindowManager);
 
         mSensitiveContentProtectionManagerService.mNotificationListener
                 .onNotificationRankingUpdate(mRankingMap);
 
-        verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET);
+        verifyZeroInteractions(mWindowManager);
     }
 
     @Test
@@ -606,13 +617,13 @@
         // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2
         // as non-sensitive
         setupSensitiveNotification();
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
         Mockito.reset(mWindowManager);
 
         mSensitiveContentProtectionManagerService.mNotificationListener
                 .onNotificationRankingUpdate(null);
 
-        verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET);
+        verifyZeroInteractions(mWindowManager);
     }
 
     @Test
@@ -620,7 +631,7 @@
         // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2
         // as non-sensitive
         setupSensitiveNotification();
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
         Mockito.reset(mWindowManager);
         when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_1))).thenReturn(null);
         doReturn(mRankingMap)
@@ -630,7 +641,7 @@
         mSensitiveContentProtectionManagerService.mNotificationListener
                 .onNotificationRankingUpdate(mRankingMap);
 
-        verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET);
+        verifyZeroInteractions(mWindowManager);
     }
 
     @Test
@@ -638,7 +649,7 @@
         // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2
         // as non-sensitive
         setupSensitiveNotification();
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
         Mockito.reset(mWindowManager);
 
         doThrow(SecurityException.class)
@@ -648,7 +659,7 @@
         mSensitiveContentProtectionManagerService.mNotificationListener
                 .onNotificationRankingUpdate(mRankingMap);
 
-        verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET);
+        verifyZeroInteractions(mWindowManager);
     }
 
     @Test
@@ -657,7 +668,7 @@
         // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2
         // as non-sensitive
         setupSensitiveNotification();
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
         mSensitiveContentProtectionManagerService.mNotificationListener
                 .onNotificationRankingUpdate(mRankingMap);
 
@@ -681,8 +692,9 @@
         // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2
         // as non-sensitive
         setupSensitiveNotification();
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
-        mMediaProjectionCallbackCaptor.getValue().onStop(mock(MediaProjectionInfo.class));
+        MediaProjectionInfo mediaProjectionInfo = createMediaProjectionInfo();
+        mMediaProjectionCallbackCaptor.getValue().onStart(mediaProjectionInfo);
+        mMediaProjectionCallbackCaptor.getValue().onStop(mediaProjectionInfo);
         Mockito.reset(mWindowManager);
 
         mSensitiveContentProtectionManagerService.mNotificationListener
@@ -696,7 +708,7 @@
         // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2
         // as non-sensitive
         setupSensitiveNotification();
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
         Mockito.reset(mWindowManager);
 
         mSensitiveContentProtectionManagerService.mNotificationListener
@@ -712,7 +724,7 @@
         // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2
         // as non-sensitive
         setupSensitiveNotification();
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
         Mockito.reset(mWindowManager);
 
         mSensitiveContentProtectionManagerService.mNotificationListener
@@ -726,7 +738,7 @@
         // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2
         // as non-sensitive
         setupSensitiveNotification();
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
         Mockito.reset(mWindowManager);
 
         mSensitiveContentProtectionManagerService.mNotificationListener
@@ -740,7 +752,7 @@
         // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2
         // as non-sensitive
         setupSensitiveNotification();
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
         Mockito.reset(mWindowManager);
 
         mSensitiveContentProtectionManagerService.mNotificationListener
@@ -754,7 +766,7 @@
         // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2
         // as non-sensitive
         setupSensitiveNotification();
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
         Mockito.reset(mWindowManager);
         when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_1))).thenReturn(null);
 
@@ -770,7 +782,7 @@
         // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2
         // as non-sensitive
         setupSensitiveNotification();
-        mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+        mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
         mSensitiveContentProtectionManagerService.mNotificationListener
                 .onNotificationPosted(mNotification1, mRankingMap);
 
diff --git a/services/tests/selinux/AndroidTest.xml b/services/tests/selinux/AndroidTest.xml
new file mode 100644
index 0000000..16d8e07
--- /dev/null
+++ b/services/tests/selinux/AndroidTest.xml
@@ -0,0 +1,33 @@
+<?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.
+-->
+<configuration description="Runs Selinux Frameworks Tests.">
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-instrumentation" />
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="install-arg" value="-t" />
+        <option name="test-file-name" value="SelinuxFrameworksTests.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="SelinuxFrameworksTests" />
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.frameworks.selinuxtests" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+</configuration>
diff --git a/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java
index e0a99b0..e189098 100644
--- a/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java
@@ -3156,6 +3156,39 @@
     }
 
     @SmallTest
+    public void testAccountRemovedBroadcastMarkedAccountAsVisibleTwice() throws Exception {
+        unlockSystemUser();
+
+        HashMap<String, Integer> visibility = new HashMap<>();
+        visibility.put("testpackage1", AccountManager.VISIBILITY_USER_MANAGED_VISIBLE);
+
+        addAccountRemovedReceiver("testpackage1");
+        mAms.registerAccountListener(
+                new String [] {AccountManagerServiceTestFixtures.ACCOUNT_TYPE_1},
+                "testpackage1");
+        mAms.addAccountExplicitlyWithVisibility(
+                AccountManagerServiceTestFixtures.ACCOUNT_SUCCESS,
+                /* password= */ "p11",
+                /* extras= */ null,
+                visibility,
+                /* callerPackage= */ null);
+
+        updateBroadcastCounters(2);
+        assertEquals(mVisibleAccountsChangedBroadcasts, 1);
+        assertEquals(mLoginAccountsChangedBroadcasts, 1);
+        assertEquals(mAccountRemovedBroadcasts, 0);
+
+        mAms.setAccountVisibility(AccountManagerServiceTestFixtures.ACCOUNT_SUCCESS,
+                "testpackage1",
+                AccountManager.VISIBILITY_VISIBLE);
+
+        updateBroadcastCounters(3);
+        assertEquals(mVisibleAccountsChangedBroadcasts, 1); // visibility was not changed
+        assertEquals(mLoginAccountsChangedBroadcasts, 2);
+        assertEquals(mAccountRemovedBroadcasts, 0);
+    }
+
+    @SmallTest
     public void testRegisterAccountListenerCredentialsUpdate() throws Exception {
         unlockSystemUser();
         mAms.registerAccountListener(
@@ -3493,6 +3526,12 @@
         }
 
         @Override
+        public boolean bindServiceAsUser(Intent service, ServiceConnection conn,
+                Context.BindServiceFlags flags, UserHandle user) {
+            return mTestContext.bindServiceAsUser(service, conn, flags, user);
+        }
+
+        @Override
         public void unbindService(ServiceConnection conn) {
             mTestContext.unbindService(conn);
         }
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
index f1c1dc3..59f4d56b 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
@@ -24,6 +24,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.any;
@@ -315,7 +316,7 @@
                                 mFakeBtDevice.getAddress()));
                 verify(mMockAudioService,
                         timeout(MAX_MESSAGE_HANDLING_DELAY_MS).times(0)).onUpdatedAdiDeviceState(
-                        eq(devState));
+                        eq(devState), anyBoolean());
             }
 
             // metadata set
@@ -326,7 +327,7 @@
                                 mFakeBtDevice.getAddress()));
                 verify(mMockAudioService,
                         timeout(MAX_MESSAGE_HANDLING_DELAY_MS)).onUpdatedAdiDeviceState(
-                        any());
+                        any(), anyBoolean());
             }
         } finally {
             // reset the metadata device type
@@ -354,7 +355,7 @@
         verify(mMockAudioService,
                 timeout(MAX_MESSAGE_HANDLING_DELAY_MS).atLeast(1)).onUpdatedAdiDeviceState(
                 ArgumentMatchers.argThat(devState -> devState.getAudioDeviceCategory()
-                        == AudioManager.AUDIO_DEVICE_CATEGORY_OTHER));
+                        == AudioManager.AUDIO_DEVICE_CATEGORY_OTHER), anyBoolean());
     }
 
     private void doTestConnectionDisconnectionReconnection(int delayAfterDisconnection,
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java
index b11f9b2..073b551 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java
@@ -31,6 +31,7 @@
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.when;
 
+import android.os.Message;
 import android.platform.test.annotations.Presubmit;
 import android.view.DisplayInfo;
 
@@ -60,6 +61,8 @@
     private int mColorMode;
     private int mLogicalDensityDpi;
 
+    private final Message mScreenUnblocker = mock(Message.class);
+
     @Override
     protected void onBeforeSystemServicesCreated() {
         // Set other flags to their default values
@@ -73,12 +76,11 @@
         doReturn(true).when(mDisplayContent).getLastHasContent();
         mockTransitionsController(/* enabled= */ true);
         mockRemoteDisplayChangeController();
+        performInitialDisplayUpdate();
     }
 
     @Test
     public void testUpdate_deferrableFieldChangedTransitionStarted_deferrableFieldUpdated() {
-        performInitialDisplayUpdate();
-
         mUniqueId = "old";
         Runnable onUpdated = mock(Runnable.class);
         mDisplayContent.requestDisplayUpdate(onUpdated);
@@ -107,8 +109,6 @@
 
     @Test
     public void testUpdate_nonDeferrableUpdateAndTransitionDeferred_nonDeferrableFieldUpdated() {
-        performInitialDisplayUpdate();
-
         // Update only color mode (non-deferrable field) and keep the same unique id
         mUniqueId = "initial_unique_id";
         mColorMode = 123;
@@ -121,8 +121,6 @@
 
     @Test
     public void testUpdate_nonDeferrableUpdateTwiceAndTransitionDeferred_fieldHasLatestValue() {
-        performInitialDisplayUpdate();
-
         // Update only color mode (non-deferrable field) and keep the same unique id
         mUniqueId = "initial_unique_id";
         mColorMode = 123;
@@ -163,7 +161,6 @@
 
     @Test
     public void testUpdate_deferrableFieldUpdatedTransitionPending_fieldNotUpdated() {
-        performInitialDisplayUpdate();
         mUniqueId = "old";
         Runnable onUpdated = mock(Runnable.class);
         mDisplayContent.requestDisplayUpdate(onUpdated);
@@ -181,7 +178,6 @@
 
     @Test
     public void testTwoDisplayUpdates_transitionStarted_displayUpdated() {
-        performInitialDisplayUpdate();
         mUniqueId = "old";
         Runnable onUpdated = mock(Runnable.class);
         mDisplayContent.requestDisplayUpdate(onUpdated);
@@ -212,6 +208,51 @@
         assertThat(mDisplayContent.getDisplayInfo().uniqueId).isEqualTo("new2");
     }
 
+    @Test
+    public void testWaitForTransition_displaySwitching_waitsForTransitionToBeStarted() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_WAIT_FOR_TRANSITION_ON_DISPLAY_SWITCH);
+        mDisplayContent.mDisplayUpdater.onDisplaySwitching(/* switching= */ true);
+        boolean willWait = mDisplayContent.mDisplayUpdater.waitForTransition(mScreenUnblocker);
+        assertThat(willWait).isTrue();
+        mUniqueId = "new";
+        mDisplayContent.requestDisplayUpdate(mock(Runnable.class));
+        when(mDisplayContent.mTransitionController.inTransition()).thenReturn(true);
+        captureStartTransitionCollection().getValue().onCollectStarted(/* deferred= */ true);
+
+        // Verify that screen is not unblocked yet as the start transaction hasn't been presented
+        verify(mScreenUnblocker, never()).sendToTarget();
+
+        when(mDisplayContent.mTransitionController.inTransition()).thenReturn(false);
+        final Transition transition = captureRequestedTransition().getValue();
+        makeTransitionTransactionCompleted(transition);
+
+        // Verify that screen is unblocked as start transaction of the transition
+        // has been completed
+        verify(mScreenUnblocker).sendToTarget();
+    }
+
+    @Test
+    public void testWaitForTransition_displayNotSwitching_doesNotWait() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_WAIT_FOR_TRANSITION_ON_DISPLAY_SWITCH);
+        mDisplayContent.mDisplayUpdater.onDisplaySwitching(/* switching= */ false);
+
+        boolean willWait = mDisplayContent.mDisplayUpdater.waitForTransition(mScreenUnblocker);
+
+        assertThat(willWait).isFalse();
+        verify(mScreenUnblocker, never()).sendToTarget();
+    }
+
+    @Test
+    public void testWaitForTransition_displayIsSwitchingButFlagDisabled_doesNotWait() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_WAIT_FOR_TRANSITION_ON_DISPLAY_SWITCH);
+        mDisplayContent.mDisplayUpdater.onDisplaySwitching(/* switching= */ true);
+
+        boolean willWait = mDisplayContent.mDisplayUpdater.waitForTransition(mScreenUnblocker);
+
+        assertThat(willWait).isFalse();
+        verify(mScreenUnblocker, never()).sendToTarget();
+    }
+
     private void mockTransitionsController(boolean enabled) {
         spyOn(mDisplayContent.mTransitionController);
         when(mDisplayContent.mTransitionController.isShellTransitionsEnabled()).thenReturn(enabled);
@@ -233,6 +274,23 @@
         return callbackCaptor;
     }
 
+    private ArgumentCaptor<Transition> captureRequestedTransition() {
+        ArgumentCaptor<Transition> callbackCaptor =
+                ArgumentCaptor.forClass(Transition.class);
+        verify(mDisplayContent.mTransitionController, atLeast(1))
+                .requestStartTransition(callbackCaptor.capture(), any(), any(), any());
+        return callbackCaptor;
+    }
+
+    private void makeTransitionTransactionCompleted(Transition transition) {
+        if (transition.mTransactionCompletedListeners != null) {
+            for (int i = 0; i < transition.mTransactionCompletedListeners.size(); i++) {
+                final Runnable listener = transition.mTransactionCompletedListeners.get(i);
+                listener.run();
+            }
+        }
+    }
+
     private void performInitialDisplayUpdate() {
         mUniqueId = "initial_unique_id";
         mColorMode = 0;
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
index 1233686..00a8842 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
@@ -167,6 +167,10 @@
     }
 
     @Override
+    public void onDisplaySwitchStart(int displayId) {
+    }
+
+    @Override
     public boolean okToAnimate(boolean ignoreScreenOn) {
         return mOkToAnimate;
     }
diff --git a/services/usb/Android.bp b/services/usb/Android.bp
index e8ffe54..e00627e 100644
--- a/services/usb/Android.bp
+++ b/services/usb/Android.bp
@@ -44,6 +44,7 @@
 aconfig_declarations {
     name: "usb_flags",
     package: "com.android.server.usb.flags",
+    container: "system",
     srcs: ["**/usb_flags.aconfig"],
 }
 
diff --git a/services/usb/java/com/android/server/usb/UsbService.java b/services/usb/java/com/android/server/usb/UsbService.java
index 9b5612e..9470c0a 100644
--- a/services/usb/java/com/android/server/usb/UsbService.java
+++ b/services/usb/java/com/android/server/usb/UsbService.java
@@ -60,6 +60,7 @@
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.content.PackageMonitor;
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.DumpUtils;
@@ -222,6 +223,21 @@
         mContext.registerReceiverAsUser(receiver, UserHandle.ALL, filter, null, null);
     }
 
+    // Ideally we should use the injector pattern so we wouldn't need this constructor  for test
+    @VisibleForTesting
+    UsbService(Context context,
+                      UsbPortManager usbPortManager,
+                      UsbAlsaManager usbAlsaManager,
+                      UserManager userManager,
+                      UsbSettingsManager usbSettingsManager) {
+        mContext = context;
+        mPortManager = usbPortManager;
+        mAlsaManager = usbAlsaManager;
+        mUserManager = userManager;
+        mSettingsManager = usbSettingsManager;
+        mPermissionManager = new UsbPermissionManager(context, this);
+    }
+
     /**
      * Set new {@link #mCurrentUserId} and propagate it to other modules.
      *
@@ -886,7 +902,16 @@
 
     @Override
     public boolean enableUsbData(String portId, boolean enable, int operationId,
-            IUsbOperationInternal callback) {
+                                 IUsbOperationInternal callback) {
+        return enableUsbDataInternal(portId, enable, operationId, callback, Binder.getCallingUid());
+    }
+
+    /**
+     *  Internal function abstracted for testing with callerUid
+     */
+    @VisibleForTesting
+    boolean enableUsbDataInternal(String portId, boolean enable, int operationId,
+            IUsbOperationInternal callback, int callerUid) {
         Objects.requireNonNull(portId, "enableUsbData: portId must not be null. opId:"
                 + operationId);
         Objects.requireNonNull(callback, "enableUsbData: callback must not be null. opId:"
@@ -894,7 +919,7 @@
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USB, null);
 
         if (android.hardware.usb.flags.Flags.enableUsbDataSignalStaking()) {
-            if (!shouldUpdateUsbSignaling(portId, enable, Binder.getCallingUid())) {
+            if (!shouldUpdateUsbSignaling(portId, enable, callerUid)) {
                 try {
                     callback.onOperationComplete(USB_OPERATION_ERROR_INTERNAL);
                 } catch (RemoteException e) {
@@ -950,7 +975,16 @@
 
     @Override
     public void enableUsbDataWhileDocked(String portId, int operationId,
-            IUsbOperationInternal callback) {
+                                         IUsbOperationInternal callback) {
+        enableUsbDataWhileDockedInternal(portId, operationId, callback, Binder.getCallingUid());
+    }
+
+    /**
+     *  Internal function abstracted for testing with callerUid
+     */
+    @VisibleForTesting
+     void enableUsbDataWhileDockedInternal(String portId, int operationId,
+            IUsbOperationInternal callback, int callerUid) {
         Objects.requireNonNull(portId, "enableUsbDataWhileDocked: portId must not be null. opId:"
                 + operationId);
         Objects.requireNonNull(callback,
@@ -959,7 +993,7 @@
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USB, null);
 
         if (android.hardware.usb.flags.Flags.enableUsbDataSignalStaking()) {
-            if (!shouldUpdateUsbSignaling(portId, true, Binder.getCallingUid())) {
+            if (!shouldUpdateUsbSignaling(portId, true, callerUid)) {
                 try {
                     callback.onOperationComplete(USB_OPERATION_ERROR_INTERNAL);
                 } catch (RemoteException e) {
diff --git a/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig b/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig
index ea6e502..a7c5ddb 100644
--- a/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig
+++ b/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig
@@ -1,4 +1,5 @@
 package: "com.android.server.usb.flags"
+container: "system"
 
 flag {
     name: "allow_restriction_of_overlay_activities"
diff --git a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl
index 36485c6..16983a0 100644
--- a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl
+++ b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl
@@ -83,6 +83,9 @@
      *
      * @param enableSatellite True to enable the satellite modem and false to disable.
      * @param enableDemoMode True to enable demo mode and false to disable.
+     * @param isEmergency To specify the satellite is enabled for emergency session and false for
+     * non emergency session. Note: it is possible that a emergency session started get converted
+     * to a non emergency session and vice versa.
      * @param resultCallback The callback to receive the error code result of the operation.
      *
      * Valid result codes returned:
@@ -96,7 +99,7 @@
      *   SatelliteResult:SATELLITE_RESULT_NO_RESOURCES
      */
     void requestSatelliteEnabled(in boolean enableSatellite, in boolean enableDemoMode,
-            in IIntegerConsumer resultCallback);
+            in boolean isEmergency, in IIntegerConsumer resultCallback);
 
     /**
      * Request to get whether the satellite modem is enabled.
diff --git a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java
index b7dc79f..a623633 100644
--- a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java
+++ b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java
@@ -90,11 +90,11 @@
 
         @Override
         public void requestSatelliteEnabled(boolean enableSatellite, boolean enableDemoMode,
-                IIntegerConsumer resultCallback) throws RemoteException {
+                boolean isEmergency, IIntegerConsumer resultCallback) throws RemoteException {
             executeMethodAsync(
                     () -> SatelliteImplBase.this
                             .requestSatelliteEnabled(
-                                    enableSatellite, enableDemoMode, resultCallback),
+                                    enableSatellite, enableDemoMode, isEmergency, resultCallback),
                     "requestSatelliteEnabled");
         }
 
@@ -337,6 +337,9 @@
      *
      * @param enableSatellite True to enable the satellite modem and false to disable.
      * @param enableDemoMode True to enable demo mode and false to disable.
+     * @param isEmergency To specify the satellite is enabled for emergency session and false for
+     * non emergency session. Note: it is possible that a emergency session started get converted
+     * to a non emergency session and vice versa.
      * @param resultCallback The callback to receive the error code result of the operation.
      *
      * Valid result codes returned:
@@ -350,7 +353,7 @@
      *   SatelliteResult:SATELLITE_RESULT_NO_RESOURCES
      */
     public void requestSatelliteEnabled(boolean enableSatellite, boolean enableDemoMode,
-            @NonNull IIntegerConsumer resultCallback) {
+            boolean isEmergency, @NonNull IIntegerConsumer resultCallback) {
         // stub implementation
     }
 
diff --git a/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java b/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java
index 081da11..489ef44 100644
--- a/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java
+++ b/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java
@@ -66,6 +66,7 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 import org.mockito.MockitoSession;
 import org.mockito.quality.Strictness;
@@ -220,43 +221,36 @@
         RescuePartyObserver rescuePartyObserver = setUpRescuePartyObserver(watchdog);
 
         verify(rescuePartyObserver, never()).executeBootLoopMitigation(1);
-        int bootCounter = 0;
+
         for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) {
             watchdog.noteBoot();
-            bootCounter += 1;
         }
+
         verify(rescuePartyObserver).executeBootLoopMitigation(1);
         verify(rescuePartyObserver, never()).executeBootLoopMitigation(2);
 
-        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
-            watchdog.noteBoot();
-            bootCounter += 1;
-        }
+        watchdog.noteBoot();
+
         verify(rescuePartyObserver).executeBootLoopMitigation(2);
         verify(rescuePartyObserver, never()).executeBootLoopMitigation(3);
 
-        int bootLoopThreshold = PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - bootCounter;
-        for (int i = 0; i < bootLoopThreshold; i++) {
-            watchdog.noteBoot();
-        }
+        watchdog.noteBoot();
+
         verify(rescuePartyObserver).executeBootLoopMitigation(3);
         verify(rescuePartyObserver, never()).executeBootLoopMitigation(4);
 
-        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
-            watchdog.noteBoot();
-        }
+        watchdog.noteBoot();
+
         verify(rescuePartyObserver).executeBootLoopMitigation(4);
         verify(rescuePartyObserver, never()).executeBootLoopMitigation(5);
 
-        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
-            watchdog.noteBoot();
-        }
+        watchdog.noteBoot();
+
         verify(rescuePartyObserver).executeBootLoopMitigation(5);
         verify(rescuePartyObserver, never()).executeBootLoopMitigation(6);
 
-        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
-            watchdog.noteBoot();
-        }
+        watchdog.noteBoot();
+
         verify(rescuePartyObserver).executeBootLoopMitigation(6);
         verify(rescuePartyObserver, never()).executeBootLoopMitigation(7);
     }
@@ -268,11 +262,11 @@
                 setUpRollbackPackageHealthObserver(watchdog);
 
         verify(rollbackObserver, never()).executeBootLoopMitigation(1);
-        int bootCounter = 0;
+
         for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) {
             watchdog.noteBoot();
-            bootCounter += 1;
         }
+
         verify(rollbackObserver).executeBootLoopMitigation(1);
         verify(rollbackObserver, never()).executeBootLoopMitigation(2);
 
@@ -280,19 +274,16 @@
         when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_HIGH,
                 ROLLBACK_INFO_MANUAL));
 
-        int bootLoopThreshold = PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - bootCounter;
-        for (int i = 0; i < bootLoopThreshold; i++) {
-            watchdog.noteBoot();
-        }
+        watchdog.noteBoot();
+
         verify(rollbackObserver).executeBootLoopMitigation(2);
         verify(rollbackObserver, never()).executeBootLoopMitigation(3);
 
         // Update the list of available rollbacks after executing bootloop mitigation once
         when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_MANUAL));
 
-        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
-            watchdog.noteBoot();
-        }
+        watchdog.noteBoot();
+
         verify(rollbackObserver, never()).executeBootLoopMitigation(3);
     }
 
@@ -305,27 +296,21 @@
 
         verify(rescuePartyObserver, never()).executeBootLoopMitigation(1);
         verify(rollbackObserver, never()).executeBootLoopMitigation(1);
-        int bootCounter = 0;
         for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) {
             watchdog.noteBoot();
-            bootCounter += 1;
         }
         verify(rescuePartyObserver).executeBootLoopMitigation(1);
         verify(rescuePartyObserver, never()).executeBootLoopMitigation(2);
         verify(rollbackObserver, never()).executeBootLoopMitigation(1);
 
-        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
-            watchdog.noteBoot();
-            bootCounter += 1;
-        }
+        watchdog.noteBoot();
+
         verify(rescuePartyObserver).executeBootLoopMitigation(2);
         verify(rescuePartyObserver, never()).executeBootLoopMitigation(3);
         verify(rollbackObserver, never()).executeBootLoopMitigation(2);
 
-        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
-            watchdog.noteBoot();
-            bootCounter += 1;
-        }
+        watchdog.noteBoot();
+
         verify(rescuePartyObserver, never()).executeBootLoopMitigation(3);
         verify(rollbackObserver).executeBootLoopMitigation(1);
         verify(rollbackObserver, never()).executeBootLoopMitigation(2);
@@ -333,43 +318,46 @@
         when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_HIGH,
                 ROLLBACK_INFO_MANUAL));
 
-        int bootLoopThreshold = PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - bootCounter;
-        for (int i = 0; i < bootLoopThreshold; i++) {
-            watchdog.noteBoot();
-        }
+        watchdog.noteBoot();
+
         verify(rescuePartyObserver).executeBootLoopMitigation(3);
         verify(rescuePartyObserver, never()).executeBootLoopMitigation(4);
         verify(rollbackObserver, never()).executeBootLoopMitigation(2);
 
-        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
-            watchdog.noteBoot();
-        }
+        watchdog.noteBoot();
+
         verify(rescuePartyObserver).executeBootLoopMitigation(4);
         verify(rescuePartyObserver, never()).executeBootLoopMitigation(5);
         verify(rollbackObserver, never()).executeBootLoopMitigation(2);
 
-        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
-            watchdog.noteBoot();
-        }
+        watchdog.noteBoot();
+
         verify(rescuePartyObserver).executeBootLoopMitigation(5);
         verify(rescuePartyObserver, never()).executeBootLoopMitigation(6);
         verify(rollbackObserver, never()).executeBootLoopMitigation(2);
 
-        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
-            watchdog.noteBoot();
-        }
+        watchdog.noteBoot();
+
         verify(rescuePartyObserver, never()).executeBootLoopMitigation(6);
         verify(rollbackObserver).executeBootLoopMitigation(2);
         verify(rollbackObserver, never()).executeBootLoopMitigation(3);
         // Update the list of available rollbacks after executing bootloop mitigation
         when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_MANUAL));
 
-        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) {
-            watchdog.noteBoot();
-        }
+        watchdog.noteBoot();
+
         verify(rescuePartyObserver).executeBootLoopMitigation(6);
         verify(rescuePartyObserver, never()).executeBootLoopMitigation(7);
         verify(rollbackObserver, never()).executeBootLoopMitigation(3);
+
+        moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_DEESCALATION_WINDOW_MS + 1);
+        Mockito.reset(rescuePartyObserver);
+
+        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) {
+            watchdog.noteBoot();
+        }
+        verify(rescuePartyObserver).executeBootLoopMitigation(1);
+        verify(rescuePartyObserver, never()).executeBootLoopMitigation(2);
     }
 
     RollbackPackageHealthObserver setUpRollbackPackageHealthObserver(PackageWatchdog watchdog) {
@@ -506,16 +494,9 @@
         }
 
         try {
-            if (Flags.recoverabilityDetection()) {
-                mSpyBootThreshold = spy(watchdog.new BootThreshold(
-                    PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
-                    PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS,
-                    PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT));
-            } else {
-                mSpyBootThreshold = spy(watchdog.new BootThreshold(
+            mSpyBootThreshold = spy(watchdog.new BootThreshold(
                     PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
                     PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS));
-            }
 
             doAnswer((Answer<Integer>) invocationOnMock -> {
                 String storedValue = mCrashRecoveryPropertiesMap
@@ -640,5 +621,16 @@
         public long uptimeMillis() {
             return mUpTimeMillis;
         }
+        public void moveTimeForward(long milliSeconds) {
+            mUpTimeMillis += milliSeconds;
+        }
+    }
+
+    private void moveTimeForwardAndDispatch(long milliSeconds) {
+        // Exhaust all due runnables now which shouldn't be executed after time-leap
+        mTestLooper.dispatchAll();
+        mTestClock.moveTimeForward(milliSeconds);
+        mTestLooper.moveTimeForward(milliSeconds);
+        mTestLooper.dispatchAll();
     }
 }
diff --git a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
index 4f27e06..093923f 100644
--- a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
+++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
@@ -45,13 +45,13 @@
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.DeviceConfig;
 import android.util.AtomicFile;
+import android.util.LongArrayQueue;
 import android.util.Xml;
-import android.utils.LongArrayQueue;
-import android.utils.XmlUtils;
 
 import androidx.test.InstrumentationRegistry;
 
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.internal.util.XmlUtils;
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.PackageWatchdog.HealthCheckState;
@@ -1224,7 +1224,7 @@
         PackageWatchdog watchdog = createWatchdog();
         TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1);
         watchdog.registerHealthObserver(bootObserver);
-        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD; i++) {
+        for (int i = 0; i < 15; i++) {
             watchdog.noteBoot();
         }
         assertThat(bootObserver.mitigatedBootLoop()).isTrue();
@@ -1262,22 +1262,6 @@
     }
 
     /**
-     * Ensure that boot loop mitigation is not done when the number of boots does not meet the
-     * threshold.
-     */
-    @Test
-    public void testBootLoopDetection_doesNotMeetThresholdRecoverabilityHighImpact() {
-        PackageWatchdog watchdog = createWatchdog();
-        TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1,
-                PackageHealthObserverImpact.USER_IMPACT_LEVEL_80);
-        watchdog.registerHealthObserver(bootObserver);
-        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - 1; i++) {
-            watchdog.noteBoot();
-        }
-        assertThat(bootObserver.mitigatedBootLoop()).isFalse();
-    }
-
-    /**
      * Ensure that boot loop mitigation is done for the observer with the lowest user impact
      */
     @Test
@@ -1306,7 +1290,7 @@
         bootObserver2.setImpact(PackageHealthObserverImpact.USER_IMPACT_LEVEL_30);
         watchdog.registerHealthObserver(bootObserver1);
         watchdog.registerHealthObserver(bootObserver2);
-        for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD; i++) {
+        for (int i = 0; i < 15; i++) {
             watchdog.noteBoot();
         }
         assertThat(bootObserver1.mitigatedBootLoop()).isTrue();
@@ -1349,9 +1333,7 @@
             watchdog.noteBoot();
         }
         for (int i = 0; i < 4; i++) {
-            for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; j++) {
                 watchdog.noteBoot();
-            }
         }
 
         moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_DEESCALATION_WINDOW_MS + 1);
@@ -1360,38 +1342,7 @@
             watchdog.noteBoot();
         }
         for (int i = 0; i < 4; i++) {
-            for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; j++) {
                 watchdog.noteBoot();
-            }
-        }
-
-        assertThat(bootObserver.mBootMitigationCounts).isEqualTo(List.of(1, 2, 3, 4, 1, 2, 3, 4));
-    }
-
-    @Test
-    public void testMultipleBootLoopMitigationRecoverabilityHighImpact() {
-        PackageWatchdog watchdog = createWatchdog();
-        TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1,
-                PackageHealthObserverImpact.USER_IMPACT_LEVEL_80);
-        watchdog.registerHealthObserver(bootObserver);
-        for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - 1; j++) {
-            watchdog.noteBoot();
-        }
-        for (int i = 0; i < 4; i++) {
-            for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; j++) {
-                watchdog.noteBoot();
-            }
-        }
-
-        moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_DEESCALATION_WINDOW_MS + 1);
-
-        for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - 1; j++) {
-            watchdog.noteBoot();
-        }
-        for (int i = 0; i < 4; i++) {
-            for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; j++) {
-                watchdog.noteBoot();
-            }
         }
 
         assertThat(bootObserver.mBootMitigationCounts).isEqualTo(List.of(1, 2, 3, 4, 1, 2, 3, 4));
@@ -1642,8 +1593,7 @@
 
         mSpyBootThreshold = spy(watchdog.new BootThreshold(
                 PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
-                PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS,
-                PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT));
+                PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS));
 
         watchdog.saveAllObserversBootMitigationCountToMetadata(filePath);
 
@@ -1798,16 +1748,9 @@
         mCrashRecoveryPropertiesMap = new HashMap<>();
 
         try {
-            if (Flags.recoverabilityDetection()) {
-                mSpyBootThreshold = spy(watchdog.new BootThreshold(
-                    PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
-                    PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS,
-                    PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT));
-            } else {
-                mSpyBootThreshold = spy(watchdog.new BootThreshold(
+            mSpyBootThreshold = spy(watchdog.new BootThreshold(
                     PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
                     PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS));
-            }
 
             doAnswer((Answer<Integer>) invocationOnMock -> {
                 String storedValue = mCrashRecoveryPropertiesMap
diff --git a/tests/UsbManagerTests/Android.bp b/tests/UsbManagerTests/Android.bp
index a16a7ea..f0bea3f 100644
--- a/tests/UsbManagerTests/Android.bp
+++ b/tests/UsbManagerTests/Android.bp
@@ -21,6 +21,7 @@
     // to get the below license kinds:
     //   SPDX-license-identifier-Apache-2.0
     default_applicable_licenses: ["frameworks_base_license"],
+    default_team: "trendy_team_android_usb",
 }
 
 android_test {
diff --git a/tests/UsbTests/Android.bp b/tests/UsbTests/Android.bp
index 92c2711..c012cce 100644
--- a/tests/UsbTests/Android.bp
+++ b/tests/UsbTests/Android.bp
@@ -21,6 +21,7 @@
     // to get the below license kinds:
     //   SPDX-license-identifier-Apache-2.0
     default_applicable_licenses: ["frameworks_base_license"],
+    default_team: "trendy_team_android_usb",
 }
 
 android_test {
@@ -36,6 +37,8 @@
         "services.usb",
         "truth",
         "UsbManagerTestLib",
+        "android.hardware.usb.flags-aconfig-java",
+        "flag-junit",
     ],
     jni_libs: [
         // Required for ExtendedMockito
diff --git a/tests/UsbTests/TEST_MAPPING b/tests/UsbTests/TEST_MAPPING
new file mode 100644
index 0000000..70134b8
--- /dev/null
+++ b/tests/UsbTests/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "postsubmit": [
+    {
+      "name": "UsbTests"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/UsbTests/src/com/android/server/usb/UsbServiceTest.java b/tests/UsbTests/src/com/android/server/usb/UsbServiceTest.java
new file mode 100644
index 0000000..b506d74
--- /dev/null
+++ b/tests/UsbTests/src/com/android/server/usb/UsbServiceTest.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.usb;
+
+import static android.hardware.usb.UsbOperationInternal.USB_OPERATION_ERROR_INTERNAL;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.hardware.usb.IUsbOperationInternal;
+import android.hardware.usb.flags.Flags;
+import android.os.RemoteException;
+import android.os.UserManager;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for {@link com.android.server.usb.UsbService}
+ */
+@RunWith(AndroidJUnit4.class)
+public class UsbServiceTest {
+
+    @Mock
+    private Context mContext;
+    @Mock
+    private UsbPortManager mUsbPortManager;
+    @Mock
+    private UsbAlsaManager mUsbAlsaManager;
+    @Mock
+    private UserManager mUserManager;
+    @Mock
+    private UsbSettingsManager mUsbSettingsManager;
+    @Mock
+    private IUsbOperationInternal mIUsbOperationInternal;
+
+    private static final String TEST_PORT_ID = "123";
+
+    private static final int TEST_TRANSACTION_ID = 1;
+
+    private static final int TEST_FIRST_CALLER_ID = 1000;
+
+    private static final int TEST_SECOND_CALLER_ID = 2000;
+
+    private UsbService mUsbService;
+
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mUsbService = new UsbService(mContext, mUsbPortManager, mUsbAlsaManager, mUserManager,
+                mUsbSettingsManager);
+    }
+
+    /**
+     * Verify enableUsbData successfully disables USB port without error
+     */
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_USB_DATA_SIGNAL_STAKING)
+    public void usbPort_SuccessfullyDisabled() {
+        boolean enableState = false;
+        when(mUsbPortManager.enableUsbData(TEST_PORT_ID, enableState, TEST_TRANSACTION_ID,
+                mIUsbOperationInternal, null)).thenReturn(true);
+
+        assertTrue(mUsbService.enableUsbDataInternal(TEST_PORT_ID, enableState,
+                TEST_TRANSACTION_ID, mIUsbOperationInternal, TEST_FIRST_CALLER_ID));
+
+        verify(mUsbPortManager, times(1)).enableUsbData(TEST_PORT_ID,
+                enableState, TEST_TRANSACTION_ID, mIUsbOperationInternal, null);
+        verifyZeroInteractions(mIUsbOperationInternal);
+    }
+
+    /**
+     * Verify enableUsbData successfully enables USB port without error given no other stakers
+     */
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_USB_DATA_SIGNAL_STAKING)
+    public void usbPortWhenNoOtherStakers_SuccessfullyEnabledUsb() {
+        boolean enableState = true;
+        when(mUsbPortManager.enableUsbData(TEST_PORT_ID, enableState, TEST_TRANSACTION_ID,
+                mIUsbOperationInternal, null))
+                .thenReturn(true);
+
+        assertTrue(mUsbService.enableUsbDataInternal(TEST_PORT_ID, enableState,
+                TEST_TRANSACTION_ID, mIUsbOperationInternal, TEST_FIRST_CALLER_ID));
+        verifyZeroInteractions(mIUsbOperationInternal);
+    }
+
+    /**
+     * Verify enableUsbData does not enable USB port if other stakers are present
+     */
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_USB_DATA_SIGNAL_STAKING)
+    public void usbPortWithOtherStakers_DoesNotToEnableUsb() throws RemoteException {
+        mUsbService.enableUsbDataInternal(TEST_PORT_ID, false, TEST_TRANSACTION_ID,
+                mIUsbOperationInternal, TEST_FIRST_CALLER_ID);
+        clearInvocations(mUsbPortManager);
+
+        assertFalse(mUsbService.enableUsbDataInternal(TEST_PORT_ID, true,
+                TEST_TRANSACTION_ID, mIUsbOperationInternal, TEST_SECOND_CALLER_ID));
+
+        verifyZeroInteractions(mUsbPortManager);
+        verify(mIUsbOperationInternal).onOperationComplete(USB_OPERATION_ERROR_INTERNAL);
+    }
+
+    /**
+     * Verify enableUsbDataWhileDockedInternal does not enable USB port if other stakers are present
+     */
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_USB_DATA_SIGNAL_STAKING)
+    public void enableUsbWhileDockedWhenThereAreOtherStakers_DoesNotEnableUsb()
+            throws RemoteException {
+        mUsbService.enableUsbDataInternal(TEST_PORT_ID, false, TEST_TRANSACTION_ID,
+                mIUsbOperationInternal, TEST_FIRST_CALLER_ID);
+
+        mUsbService.enableUsbDataWhileDockedInternal(TEST_PORT_ID, 0,
+                mIUsbOperationInternal, TEST_SECOND_CALLER_ID);
+
+        verify(mUsbPortManager, never()).enableUsbDataWhileDocked(any(),
+                anyLong(), any(), any());
+        verify(mIUsbOperationInternal).onOperationComplete(USB_OPERATION_ERROR_INTERNAL);
+    }
+
+    /**
+     * Verify enableUsbDataWhileDockedInternal does  enable USB port if other stakers are
+     * not present
+     */
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_USB_DATA_SIGNAL_STAKING)
+    public void enableUsbWhileDockedWhenThereAreNoStakers_SuccessfullyEnableUsb()
+            throws RemoteException {
+        mUsbService.enableUsbDataWhileDockedInternal(TEST_PORT_ID, TEST_TRANSACTION_ID,
+                mIUsbOperationInternal, TEST_SECOND_CALLER_ID);
+
+        verify(mUsbPortManager, times(1))
+                .enableUsbDataWhileDocked(TEST_PORT_ID, TEST_TRANSACTION_ID,
+                        mIUsbOperationInternal, null);
+        verifyZeroInteractions(mIUsbOperationInternal);
+    }
+}
diff --git a/tools/app_metadata_bundles/Android.bp b/tools/app_metadata_bundles/Android.bp
index a012dca..dced50d 100644
--- a/tools/app_metadata_bundles/Android.bp
+++ b/tools/app_metadata_bundles/Android.bp
@@ -13,6 +13,9 @@
     srcs: [
         "src/lib/java/**/*.java",
     ],
+    static_libs: [
+        "guava",
+    ],
 }
 
 java_binary_host {
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslConverter.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslConverter.java
index 9dd5531..c1c520e 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslConverter.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslConverter.java
@@ -16,7 +16,10 @@
 
 package com.android.asllib;
 
+import com.android.asllib.marshallable.AndroidSafetyLabel;
+import com.android.asllib.marshallable.AndroidSafetyLabelFactory;
 import com.android.asllib.util.MalformedXmlException;
+import com.android.asllib.util.XmlUtils;
 
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
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
deleted file mode 100644
index a0a7537..0000000
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeConstants.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.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/AndroidSafetyLabel.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AndroidSafetyLabel.java
similarity index 96%
rename from tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java
rename to tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AndroidSafetyLabel.java
index cdb559b..112b92c 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AndroidSafetyLabel.java
@@ -14,7 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.asllib;
+package com.android.asllib.marshallable;
+
+import com.android.asllib.util.XmlUtils;
 
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabelFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AndroidSafetyLabelFactory.java
similarity index 96%
rename from tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabelFactory.java
rename to tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AndroidSafetyLabelFactory.java
index 3dc725b..b69c30f 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabelFactory.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AndroidSafetyLabelFactory.java
@@ -14,9 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.asllib;
+package com.android.asllib.marshallable;
 
 import com.android.asllib.util.MalformedXmlException;
+import com.android.asllib.util.XmlUtils;
 
 import org.w3c.dom.Element;
 
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AppInfo.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfo.java
similarity index 98%
rename from tools/app_metadata_bundles/src/lib/java/com/android/asllib/AppInfo.java
rename to tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfo.java
index f94b659..3f1ddeb 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AppInfo.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfo.java
@@ -14,7 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.asllib;
+package com.android.asllib.marshallable;
+
+import com.android.asllib.util.XmlUtils;
 
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AppInfoFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfoFactory.java
similarity index 74%
rename from tools/app_metadata_bundles/src/lib/java/com/android/asllib/AppInfoFactory.java
rename to tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfoFactory.java
index 26d94c1..59a437d 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AppInfoFactory.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfoFactory.java
@@ -14,14 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.asllib;
+package com.android.asllib.marshallable;
 
 import com.android.asllib.util.AslgenUtil;
 import com.android.asllib.util.MalformedXmlException;
+import com.android.asllib.util.XmlUtils;
 
 import org.w3c.dom.Element;
 
-import java.util.Arrays;
 import java.util.List;
 
 public class AppInfoFactory implements AslMarshallableFactory<AppInfo> {
@@ -37,31 +37,22 @@
 
         String title = XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_TITLE);
         String description = XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_DESCRIPTION);
-        Boolean containsAds = XmlUtils.getBoolAttr(appInfoEle, XmlUtils.HR_ATTR_CONTAINS_ADS);
-        Boolean obeyAps = XmlUtils.getBoolAttr(appInfoEle, XmlUtils.HR_ATTR_OBEY_APS);
+        Boolean containsAds = XmlUtils.getBoolAttr(appInfoEle, XmlUtils.HR_ATTR_CONTAINS_ADS, true);
+        Boolean obeyAps = XmlUtils.getBoolAttr(appInfoEle, XmlUtils.HR_ATTR_OBEY_APS, true);
         Boolean adsFingerprinting =
-                XmlUtils.getBoolAttr(appInfoEle, XmlUtils.HR_ATTR_ADS_FINGERPRINTING);
+                XmlUtils.getBoolAttr(appInfoEle, XmlUtils.HR_ATTR_ADS_FINGERPRINTING, true);
         Boolean securityFingerprinting =
-                XmlUtils.getBoolAttr(appInfoEle, XmlUtils.HR_ATTR_SECURITY_FINGERPRINTING);
+                XmlUtils.getBoolAttr(appInfoEle, XmlUtils.HR_ATTR_SECURITY_FINGERPRINTING, true);
         String privacyPolicy = XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_PRIVACY_POLICY);
         List<String> securityEndpoints =
-                Arrays.stream(
-                                appInfoEle
-                                        .getAttribute(XmlUtils.HR_ATTR_SECURITY_ENDPOINTS)
-                                        .split("\\|"))
-                        .toList();
+                XmlUtils.getPipelineSplitAttr(
+                        appInfoEle, XmlUtils.HR_ATTR_SECURITY_ENDPOINTS, true);
         List<String> firstPartyEndpoints =
-                Arrays.stream(
-                                appInfoEle
-                                        .getAttribute(XmlUtils.HR_ATTR_FIRST_PARTY_ENDPOINTS)
-                                        .split("\\|"))
-                        .toList();
+                XmlUtils.getPipelineSplitAttr(
+                        appInfoEle, XmlUtils.HR_ATTR_FIRST_PARTY_ENDPOINTS, true);
         List<String> serviceProviderEndpoints =
-                Arrays.stream(
-                                appInfoEle
-                                        .getAttribute(XmlUtils.HR_ATTR_SERVICE_PROVIDER_ENDPOINTS)
-                                        .split("\\|"))
-                        .toList();
+                XmlUtils.getPipelineSplitAttr(
+                        appInfoEle, XmlUtils.HR_ATTR_SERVICE_PROVIDER_ENDPOINTS, true);
         String category = XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_CATEGORY);
         String email = XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_EMAIL);
         String website = XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_WEBSITE, false);
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallable.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AslMarshallable.java
similarity index 95%
rename from tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallable.java
rename to tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AslMarshallable.java
index 4e64ab0..48747cc 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallable.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AslMarshallable.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.asllib;
+package com.android.asllib.marshallable;
 
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallableFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AslMarshallableFactory.java
similarity index 95%
rename from tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallableFactory.java
rename to tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AslMarshallableFactory.java
index b8f9f0e..a49b3e7 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallableFactory.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AslMarshallableFactory.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.asllib;
+package com.android.asllib.marshallable;
 
 import com.android.asllib.util.MalformedXmlException;
 
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/marshallable/DataCategory.java
similarity index 91%
rename from tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java
rename to tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataCategory.java
index b9e06fb..4d67162 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataCategory.java
@@ -14,7 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.asllib;
+package com.android.asllib.marshallable;
+
+import com.android.asllib.util.DataCategoryConstants;
+import com.android.asllib.util.DataTypeConstants;
+import com.android.asllib.util.XmlUtils;
 
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataCategoryFactory.java
similarity index 62%
rename from tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryFactory.java
rename to tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataCategoryFactory.java
index d2b6712..37d99e7 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryFactory.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataCategoryFactory.java
@@ -14,9 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.asllib;
+package com.android.asllib.marshallable;
 
+import com.android.asllib.util.DataTypeConstants;
 import com.android.asllib.util.MalformedXmlException;
+import com.android.asllib.util.XmlUtils;
 
 import org.w3c.dom.Element;
 
@@ -30,11 +32,17 @@
         String categoryName = null;
         Map<String, DataType> dataTypeMap = new LinkedHashMap<String, DataType>();
         for (Element ele : elements) {
-            categoryName = ele.getAttribute(XmlUtils.HR_ATTR_DATA_CATEGORY);
-            String dataTypeName = ele.getAttribute(XmlUtils.HR_ATTR_DATA_TYPE);
-            if (!DataTypeConstants.getValidDataTypes().contains(dataTypeName)) {
+            categoryName = XmlUtils.getStringAttr(ele, XmlUtils.HR_ATTR_DATA_CATEGORY, true);
+            String dataTypeName = XmlUtils.getStringAttr(ele, XmlUtils.HR_ATTR_DATA_TYPE, true);
+            if (!DataTypeConstants.getValidDataTypes().containsKey(categoryName)) {
                 throw new MalformedXmlException(
-                        String.format("Unrecognized data type name: %s", dataTypeName));
+                        String.format("Unrecognized data category %s", categoryName));
+            }
+            if (!DataTypeConstants.getValidDataTypes().get(categoryName).contains(dataTypeName)) {
+                throw new MalformedXmlException(
+                        String.format(
+                                "Unrecognized data type name %s for category %s",
+                                dataTypeName, categoryName));
             }
             dataTypeMap.put(
                     dataTypeName, new DataTypeFactory().createFromHrElements(XmlUtils.listOf(ele)));
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/marshallable/DataLabels.java
similarity index 85%
rename from tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java
rename to tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataLabels.java
index 96ec93c..7516faf 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataLabels.java
@@ -14,7 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.asllib;
+package com.android.asllib.marshallable;
+
+import com.android.asllib.util.DataCategoryConstants;
+import com.android.asllib.util.XmlUtils;
 
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
@@ -41,24 +44,23 @@
     }
 
     /**
-     * Returns the data accessed {@link Map} of {@link com.android.asllib.DataCategoryConstants} to
-     * {@link DataCategory}
+     * Returns the data accessed {@link Map} of {@link 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}
+     * Returns the data collected {@link Map} of {@link 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}
+     * Returns the data shared {@link Map} of {@link DataCategoryConstants} to {@link DataCategory}
      */
     public Map<String, DataCategory> getDataShared() {
         return mDataShared;
@@ -70,7 +72,7 @@
         Element dataLabelsEle =
                 XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_DATA_LABELS);
 
-        maybeAppendDataUsages(doc, dataLabelsEle, mDataCollected, XmlUtils.OD_NAME_DATA_ACCESSED);
+        maybeAppendDataUsages(doc, dataLabelsEle, mDataAccessed, XmlUtils.OD_NAME_DATA_ACCESSED);
         maybeAppendDataUsages(doc, dataLabelsEle, mDataCollected, XmlUtils.OD_NAME_DATA_COLLECTED);
         maybeAppendDataUsages(doc, dataLabelsEle, mDataShared, XmlUtils.OD_NAME_DATA_SHARED);
 
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabelsFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataLabelsFactory.java
similarity index 97%
rename from tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabelsFactory.java
rename to tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataLabelsFactory.java
index 79edab7..dc77fd0 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabelsFactory.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataLabelsFactory.java
@@ -14,10 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.asllib;
+package com.android.asllib.marshallable;
 
 import com.android.asllib.util.AslgenUtil;
+import com.android.asllib.util.DataCategoryConstants;
 import com.android.asllib.util.MalformedXmlException;
+import com.android.asllib.util.XmlUtils;
 
 import org.w3c.dom.Element;
 import org.w3c.dom.NodeList;
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/marshallable/DataType.java
similarity index 98%
rename from tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java
rename to tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataType.java
index d011cfe..3471362 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataType.java
@@ -14,7 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.asllib;
+package com.android.asllib.marshallable;
+
+import com.android.asllib.util.XmlUtils;
 
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataTypeFactory.java
similarity index 67%
rename from tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeFactory.java
rename to tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataTypeFactory.java
index 27c1b59..ed434cd 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeFactory.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataTypeFactory.java
@@ -14,30 +14,40 @@
  * limitations under the License.
  */
 
-package com.android.asllib;
+package com.android.asllib.marshallable;
+
+import com.android.asllib.util.MalformedXmlException;
+import com.android.asllib.util.XmlUtils;
 
 import org.w3c.dom.Element;
 
-import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
 import java.util.stream.Collectors;
 
 public class DataTypeFactory implements AslMarshallableFactory<DataType> {
     /** Creates a {@link DataType} from the human-readable DOM element. */
     @Override
-    public DataType createFromHrElements(List<Element> elements) {
+    public DataType createFromHrElements(List<Element> elements) throws MalformedXmlException {
         Element hrDataTypeEle = XmlUtils.getSingleElement(elements);
         String dataTypeName = hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_DATA_TYPE);
         List<DataType.Purpose> purposes =
-                Arrays.stream(hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_PURPOSES).split("\\|"))
+                XmlUtils.getPipelineSplitAttr(hrDataTypeEle, XmlUtils.HR_ATTR_PURPOSES, true)
+                        .stream()
                         .map(DataType.Purpose::forString)
                         .collect(Collectors.toList());
+        if (purposes.isEmpty()) {
+            throw new MalformedXmlException(String.format("Found no purpose in: %s", dataTypeName));
+        }
+        if (new HashSet<>(purposes).size() != purposes.size()) {
+            throw new MalformedXmlException(
+                    String.format("Found non-unique purposes in: %s", dataTypeName));
+        }
         Boolean isCollectionOptional =
-                XmlUtils.getBoolAttr(hrDataTypeEle, XmlUtils.HR_ATTR_IS_COLLECTION_OPTIONAL);
+                XmlUtils.getBoolAttr(hrDataTypeEle, XmlUtils.HR_ATTR_IS_COLLECTION_OPTIONAL, false);
         Boolean isSharingOptional =
-                XmlUtils.getBoolAttr(hrDataTypeEle, XmlUtils.HR_ATTR_IS_SHARING_OPTIONAL);
-        Boolean ephemeral = XmlUtils.getBoolAttr(hrDataTypeEle, XmlUtils.HR_ATTR_EPHEMERAL);
+                XmlUtils.getBoolAttr(hrDataTypeEle, XmlUtils.HR_ATTR_IS_SHARING_OPTIONAL, false);
+        Boolean ephemeral = XmlUtils.getBoolAttr(hrDataTypeEle, XmlUtils.HR_ATTR_EPHEMERAL, false);
         return new DataType(
                 dataTypeName, purposes, isCollectionOptional, isSharingOptional, ephemeral);
     }
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DeveloperInfo.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DeveloperInfo.java
similarity index 98%
rename from tools/app_metadata_bundles/src/lib/java/com/android/asllib/DeveloperInfo.java
rename to tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DeveloperInfo.java
index 44a5b12..382a1f0 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DeveloperInfo.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DeveloperInfo.java
@@ -14,7 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.asllib;
+package com.android.asllib.marshallable;
+
+import com.android.asllib.util.XmlUtils;
 
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DeveloperInfoFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DeveloperInfoFactory.java
similarity index 96%
rename from tools/app_metadata_bundles/src/lib/java/com/android/asllib/DeveloperInfoFactory.java
rename to tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DeveloperInfoFactory.java
index 4961892..b5310ba 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DeveloperInfoFactory.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DeveloperInfoFactory.java
@@ -14,10 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.asllib;
+package com.android.asllib.marshallable;
 
 import com.android.asllib.util.AslgenUtil;
 import com.android.asllib.util.MalformedXmlException;
+import com.android.asllib.util.XmlUtils;
 
 import org.w3c.dom.Element;
 
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/marshallable/SafetyLabels.java
similarity index 95%
rename from tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java
rename to tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SafetyLabels.java
index 40ef48d..22c3fd8 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SafetyLabels.java
@@ -14,7 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.asllib;
+package com.android.asllib.marshallable;
+
+import com.android.asllib.util.XmlUtils;
 
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabelsFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SafetyLabelsFactory.java
similarity index 88%
rename from tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabelsFactory.java
rename to tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SafetyLabelsFactory.java
index ab81b1d..6bf8ef3 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabelsFactory.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SafetyLabelsFactory.java
@@ -14,10 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.asllib;
+package com.android.asllib.marshallable;
 
 import com.android.asllib.util.AslgenUtil;
 import com.android.asllib.util.MalformedXmlException;
+import com.android.asllib.util.XmlUtils;
 
 import org.w3c.dom.Element;
 
@@ -40,7 +41,9 @@
                         .createFromHrElements(
                                 XmlUtils.listOf(
                                         XmlUtils.getSingleChildElement(
-                                                safetyLabelsEle, XmlUtils.HR_TAG_DATA_LABELS)));
+                                                safetyLabelsEle,
+                                                XmlUtils.HR_TAG_DATA_LABELS,
+                                                false)));
         return new SafetyLabels(version, dataLabels);
     }
 }
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SystemAppSafetyLabel.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SystemAppSafetyLabel.java
similarity index 94%
rename from tools/app_metadata_bundles/src/lib/java/com/android/asllib/SystemAppSafetyLabel.java
rename to tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SystemAppSafetyLabel.java
index 93d9c2b..595d748 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SystemAppSafetyLabel.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SystemAppSafetyLabel.java
@@ -14,7 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.asllib;
+package com.android.asllib.marshallable;
+
+import com.android.asllib.util.XmlUtils;
 
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SystemAppSafetyLabelFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SystemAppSafetyLabelFactory.java
similarity index 94%
rename from tools/app_metadata_bundles/src/lib/java/com/android/asllib/SystemAppSafetyLabelFactory.java
rename to tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SystemAppSafetyLabelFactory.java
index c8c1c7b..f999559 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SystemAppSafetyLabelFactory.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SystemAppSafetyLabelFactory.java
@@ -14,10 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.asllib;
+package com.android.asllib.marshallable;
 
 import com.android.asllib.util.AslgenUtil;
 import com.android.asllib.util.MalformedXmlException;
+import com.android.asllib.util.XmlUtils;
 
 import org.w3c.dom.Element;
 
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/TransparencyInfo.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/TransparencyInfo.java
similarity index 95%
rename from tools/app_metadata_bundles/src/lib/java/com/android/asllib/TransparencyInfo.java
rename to tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/TransparencyInfo.java
index 88717b9..ddd3557 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/TransparencyInfo.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/TransparencyInfo.java
@@ -14,7 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.asllib;
+package com.android.asllib.marshallable;
+
+import com.android.asllib.util.XmlUtils;
 
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/TransparencyInfoFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/TransparencyInfoFactory.java
similarity index 95%
rename from tools/app_metadata_bundles/src/lib/java/com/android/asllib/TransparencyInfoFactory.java
rename to tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/TransparencyInfoFactory.java
index 13a7eb6..d9c2af4 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/TransparencyInfoFactory.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/TransparencyInfoFactory.java
@@ -14,10 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.asllib;
+package com.android.asllib.marshallable;
 
 import com.android.asllib.util.AslgenUtil;
 import com.android.asllib.util.MalformedXmlException;
+import com.android.asllib.util.XmlUtils;
 
 import org.w3c.dom.Element;
 
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/util/DataCategoryConstants.java
similarity index 94%
rename from tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryConstants.java
rename to tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/DataCategoryConstants.java
index b364c8b..b5ae54c 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryConstants.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/DataCategoryConstants.java
@@ -14,8 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.asllib;
+package com.android.asllib.util;
 
+import com.android.asllib.marshallable.DataCategory;
+import com.android.asllib.marshallable.DataType;
+import com.android.asllib.marshallable.SafetyLabels;
 
 import java.util.Arrays;
 import java.util.Collections;
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/DataTypeConstants.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/DataTypeConstants.java
new file mode 100644
index 0000000..358d575
--- /dev/null
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/DataTypeConstants.java
@@ -0,0 +1,197 @@
+/*
+ * 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.util;
+
+import com.android.asllib.marshallable.DataCategory;
+import com.android.asllib.marshallable.DataType;
+import com.android.asllib.marshallable.SafetyLabels;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+import java.util.Map;
+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_PHYSICAL_ADDRESS = "physical_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 */
+    private static final Map<String, Set<String>> VALID_DATA_TYPES =
+            new ImmutableMap.Builder<String, Set<String>>()
+                    .put(
+                            DataCategoryConstants.CATEGORY_PERSONAL,
+                            ImmutableSet.of(
+                                    DataTypeConstants.TYPE_NAME,
+                                    DataTypeConstants.TYPE_EMAIL_ADDRESS,
+                                    DataTypeConstants.TYPE_PHYSICAL_ADDRESS,
+                                    DataTypeConstants.TYPE_PHONE_NUMBER,
+                                    DataTypeConstants.TYPE_RACE_ETHNICITY,
+                                    DataTypeConstants.TYPE_POLITICAL_OR_RELIGIOUS_BELIEFS,
+                                    DataTypeConstants.TYPE_SEXUAL_ORIENTATION_OR_GENDER_IDENTITY,
+                                    DataTypeConstants.TYPE_PERSONAL_IDENTIFIERS,
+                                    DataTypeConstants.TYPE_OTHER))
+                    .put(
+                            DataCategoryConstants.CATEGORY_FINANCIAL,
+                            ImmutableSet.of(
+                                    DataTypeConstants.TYPE_CARD_BANK_ACCOUNT,
+                                    DataTypeConstants.TYPE_PURCHASE_HISTORY,
+                                    DataTypeConstants.TYPE_CREDIT_SCORE,
+                                    DataTypeConstants.TYPE_OTHER))
+                    .put(
+                            DataCategoryConstants.CATEGORY_LOCATION,
+                            ImmutableSet.of(
+                                    DataTypeConstants.TYPE_APPROX_LOCATION,
+                                    DataTypeConstants.TYPE_PRECISE_LOCATION))
+                    .put(
+                            DataCategoryConstants.CATEGORY_EMAIL_TEXT_MESSAGE,
+                            ImmutableSet.of(
+                                    DataTypeConstants.TYPE_EMAILS,
+                                    DataTypeConstants.TYPE_TEXT_MESSAGES,
+                                    DataTypeConstants.TYPE_OTHER))
+                    .put(
+                            DataCategoryConstants.CATEGORY_PHOTO_VIDEO,
+                            ImmutableSet.of(
+                                    DataTypeConstants.TYPE_PHOTOS, DataTypeConstants.TYPE_VIDEOS))
+                    .put(
+                            DataCategoryConstants.CATEGORY_AUDIO,
+                            ImmutableSet.of(
+                                    DataTypeConstants.TYPE_SOUND_RECORDINGS,
+                                    DataTypeConstants.TYPE_MUSIC_FILES,
+                                    DataTypeConstants.TYPE_OTHER))
+                    .put(
+                            DataCategoryConstants.CATEGORY_STORAGE,
+                            ImmutableSet.of(DataTypeConstants.TYPE_FILES_DOCS))
+                    .put(
+                            DataCategoryConstants.CATEGORY_HEALTH_FITNESS,
+                            ImmutableSet.of(
+                                    DataTypeConstants.TYPE_HEALTH, DataTypeConstants.TYPE_FITNESS))
+                    .put(
+                            DataCategoryConstants.CATEGORY_CONTACTS,
+                            ImmutableSet.of(DataTypeConstants.TYPE_CONTACTS))
+                    .put(
+                            DataCategoryConstants.CATEGORY_CALENDAR,
+                            ImmutableSet.of(DataTypeConstants.TYPE_CALENDAR))
+                    .put(
+                            DataCategoryConstants.CATEGORY_IDENTIFIERS,
+                            ImmutableSet.of(DataTypeConstants.TYPE_OTHER))
+                    .put(
+                            DataCategoryConstants.CATEGORY_APP_PERFORMANCE,
+                            ImmutableSet.of(
+                                    DataTypeConstants.TYPE_CRASH_LOGS,
+                                    DataTypeConstants.TYPE_PERFORMANCE_DIAGNOSTICS,
+                                    DataTypeConstants.TYPE_OTHER))
+                    .put(
+                            DataCategoryConstants.CATEGORY_ACTIONS_IN_APP,
+                            ImmutableSet.of(
+                                    DataTypeConstants.TYPE_USER_INTERACTION,
+                                    DataTypeConstants.TYPE_IN_APP_SEARCH_HISTORY,
+                                    DataTypeConstants.TYPE_INSTALLED_APPS,
+                                    DataTypeConstants.TYPE_USER_GENERATED_CONTENT,
+                                    DataTypeConstants.TYPE_OTHER))
+                    .put(
+                            DataCategoryConstants.CATEGORY_SEARCH_AND_BROWSING,
+                            ImmutableSet.of(DataTypeConstants.TYPE_WEB_BROWSING_HISTORY))
+                    .buildOrThrow();
+
+    /** Returns {@link Set} of valid {@link String} category keys */
+    public static Map<String, Set<String>> getValidDataTypes() {
+        return VALID_DATA_TYPES;
+    }
+
+    private DataTypeConstants() {
+        /* do nothing - hide constructor */
+    }
+}
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/util/XmlUtils.java
similarity index 91%
rename from tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java
rename to tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/XmlUtils.java
index cc8fe79..691f92f 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/XmlUtils.java
@@ -14,9 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.asllib;
-
-import com.android.asllib.util.MalformedXmlException;
+package com.android.asllib.util;
 
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
@@ -259,21 +257,42 @@
     }
 
     /** Tries getting required version attribute and throws exception if it doesn't exist */
-    public static Long tryGetVersion(Element ele) {
+    public static Long tryGetVersion(Element ele) throws MalformedXmlException {
         long version;
         try {
             version = Long.parseLong(ele.getAttribute(XmlUtils.HR_ATTR_VERSION));
         } catch (Exception e) {
-            throw new IllegalArgumentException(
+            throw new MalformedXmlException(
                     String.format(
                             "Malformed or missing required version in: %s", ele.getTagName()));
         }
         return version;
     }
 
-    /** Gets an optional Boolean attribute. */
-    public static Boolean getBoolAttr(Element ele, String attrName) {
-        return XmlUtils.fromString(ele.getAttribute(attrName));
+    /** Gets a pipeline-split attribute. */
+    public static List<String> getPipelineSplitAttr(Element ele, String attrName, boolean required)
+            throws MalformedXmlException {
+        List<String> list = Arrays.stream(ele.getAttribute(attrName).split("\\|")).toList();
+        if ((list.isEmpty() || list.get(0).isEmpty()) && required) {
+            throw new MalformedXmlException(
+                    String.format(
+                            "Delimited string %s was required but missing, in %s.",
+                            attrName, ele.getTagName()));
+        }
+        return list;
+    }
+
+    /** Gets a Boolean attribute. */
+    public static Boolean getBoolAttr(Element ele, String attrName, boolean required)
+            throws MalformedXmlException {
+        Boolean b = XmlUtils.fromString(ele.getAttribute(attrName));
+        if (b == null && required) {
+            throw new MalformedXmlException(
+                    String.format(
+                            "Boolean %s was required but missing, in %s.",
+                            attrName, ele.getTagName()));
+        }
+        return b;
     }
 
     /** Gets a required String attribute. */
diff --git a/tools/app_metadata_bundles/src/test/java/com/android/aslgen/AslgenTests.java b/tools/app_metadata_bundles/src/test/java/com/android/aslgen/AslgenTests.java
deleted file mode 100644
index 3026f8b..0000000
--- a/tools/app_metadata_bundles/src/test/java/com/android/aslgen/AslgenTests.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.aslgen;
-
-import static org.junit.Assert.assertEquals;
-
-import com.android.asllib.AndroidSafetyLabel;
-import com.android.asllib.AslConverter;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.w3c.dom.Document;
-import org.xml.sax.SAXException;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.List;
-
-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;
-
-@RunWith(JUnit4.class)
-public class AslgenTests {
-    private static final String VALID_MAPPINGS_PATH = "com/android/aslgen/validmappings";
-    private static final List<String> VALID_MAPPINGS_SUBDIRS = List.of("location", "contacts");
-    private static final String HR_XML_FILENAME = "hr.xml";
-    private static final String OD_XML_FILENAME = "od.xml";
-
-    /** Logic for setting up tests (empty if not yet needed). */
-    public static void main(String[] params) throws Exception {}
-
-    /** Tests valid mappings between HR and OD. */
-    @Test
-    public void testValidMappings() throws Exception {
-        System.out.println("start testing valid mappings.");
-
-        for (String subdir : VALID_MAPPINGS_SUBDIRS) {
-            Path hrPath = Paths.get(VALID_MAPPINGS_PATH, subdir, HR_XML_FILENAME);
-            Path odPath = Paths.get(VALID_MAPPINGS_PATH, subdir, OD_XML_FILENAME);
-
-            System.out.println("hr path: " + hrPath.toString());
-            System.out.println("od path: " + odPath.toString());
-
-            InputStream hrStream =
-                    getClass().getClassLoader().getResourceAsStream(hrPath.toString());
-            String hrContents = new String(hrStream.readAllBytes(), StandardCharsets.UTF_8);
-            InputStream odStream =
-                    getClass().getClassLoader().getResourceAsStream(odPath.toString());
-            String odContents = new String(odStream.readAllBytes(), StandardCharsets.UTF_8);
-            AndroidSafetyLabel asl =
-                    AslConverter.readFromString(hrContents, AslConverter.Format.HUMAN_READABLE);
-            String out = AslConverter.getXmlAsString(asl, AslConverter.Format.ON_DEVICE);
-            System.out.println("out: " + out);
-
-            assertEquals(getFormattedXml(out), getFormattedXml(odContents));
-        }
-    }
-
-    private static String getFormattedXml(String xmlStr)
-            throws ParserConfigurationException, IOException, SAXException, TransformerException {
-        InputStream stream = new ByteArrayInputStream(xmlStr.getBytes(StandardCharsets.UTF_8));
-        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
-        factory.setNamespaceAware(true);
-        Document document = factory.newDocumentBuilder().parse(stream);
-
-        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, "no");
-
-        ByteArrayOutputStream outStream = new ByteArrayOutputStream();
-        StreamResult streamResult = new StreamResult(outStream); // out
-        DOMSource domSource = new DOMSource(document);
-        transformer.transform(domSource, streamResult);
-
-        return outStream.toString(StandardCharsets.UTF_8);
-    }
-}
diff --git a/tools/app_metadata_bundles/src/test/java/com/android/aslgen/AllTests.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/AllTests.java
similarity index 67%
copy from tools/app_metadata_bundles/src/test/java/com/android/aslgen/AllTests.java
copy to tools/app_metadata_bundles/src/test/java/com/android/asllib/AllTests.java
index 7ebb7a1..03e8ac6 100644
--- a/tools/app_metadata_bundles/src/test/java/com/android/aslgen/AllTests.java
+++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/AllTests.java
@@ -14,7 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.aslgen;
+package com.android.asllib;
+
+import com.android.asllib.marshallable.AndroidSafetyLabelTest;
+import com.android.asllib.marshallable.DataCategoryTest;
+import com.android.asllib.marshallable.DataLabelsTest;
+import com.android.asllib.marshallable.DeveloperInfoTest;
 
 import org.junit.runner.RunWith;
 import org.junit.runners.Suite;
@@ -22,5 +27,9 @@
 @RunWith(Suite.class)
 @Suite.SuiteClasses({
     AslgenTests.class,
+    AndroidSafetyLabelTest.class,
+    DeveloperInfoTest.class,
+    DataCategoryTest.class,
+    DataLabelsTest.class,
 })
 public class AllTests {}
diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/AslgenTests.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/AslgenTests.java
new file mode 100644
index 0000000..5f43008
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/AslgenTests.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.asllib.marshallable.AndroidSafetyLabel;
+import com.android.asllib.testutils.TestUtils;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class AslgenTests {
+    private static final String VALID_MAPPINGS_PATH = "com/android/asllib/validmappings";
+    private static final List<String> VALID_MAPPINGS_SUBDIRS = List.of("location", "contacts");
+    private static final String HR_XML_FILENAME = "hr.xml";
+    private static final String OD_XML_FILENAME = "od.xml";
+
+    /** Logic for setting up tests (empty if not yet needed). */
+    public static void main(String[] params) throws Exception {}
+
+    /** Tests valid mappings between HR and OD. */
+    @Test
+    public void testValidMappings() throws Exception {
+        System.out.println("start testing valid mappings.");
+
+        for (String subdir : VALID_MAPPINGS_SUBDIRS) {
+            Path hrPath = Paths.get(VALID_MAPPINGS_PATH, subdir, HR_XML_FILENAME);
+            Path odPath = Paths.get(VALID_MAPPINGS_PATH, subdir, OD_XML_FILENAME);
+
+            System.out.println("hr path: " + hrPath.toString());
+            System.out.println("od path: " + odPath.toString());
+
+            InputStream hrStream =
+                    getClass().getClassLoader().getResourceAsStream(hrPath.toString());
+            String hrContents = new String(hrStream.readAllBytes(), StandardCharsets.UTF_8);
+            InputStream odStream =
+                    getClass().getClassLoader().getResourceAsStream(odPath.toString());
+            String odContents = new String(odStream.readAllBytes(), StandardCharsets.UTF_8);
+            AndroidSafetyLabel asl =
+                    AslConverter.readFromString(hrContents, AslConverter.Format.HUMAN_READABLE);
+            String out = AslConverter.getXmlAsString(asl, AslConverter.Format.ON_DEVICE);
+            System.out.println("out: " + out);
+
+            assertEquals(
+                    TestUtils.getFormattedXml(out, false),
+                    TestUtils.getFormattedXml(odContents, false));
+        }
+    }
+}
diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AndroidSafetyLabelTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AndroidSafetyLabelTest.java
new file mode 100644
index 0000000..0137007
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AndroidSafetyLabelTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib.marshallable;
+
+import com.android.asllib.testutils.TestUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.w3c.dom.Document;
+
+@RunWith(JUnit4.class)
+public class AndroidSafetyLabelTest {
+    private static final String ANDROID_SAFETY_LABEL_HR_PATH =
+            "com/android/asllib/androidsafetylabel/hr";
+    private static final String ANDROID_SAFETY_LABEL_OD_PATH =
+            "com/android/asllib/androidsafetylabel/od";
+
+    private static final String MISSING_VERSION_FILE_NAME = "missing-version.xml";
+    private static final String VALID_EMPTY_FILE_NAME = "valid-empty.xml";
+    private static final String WITH_SAFETY_LABELS_FILE_NAME = "with-safety-labels.xml";
+    private static final String WITH_SYSTEM_APP_SAFETY_LABEL_FILE_NAME =
+            "with-system-app-safety-label.xml";
+    private static final String WITH_TRANSPARENCY_INFO_FILE_NAME = "with-transparency-info.xml";
+
+    private Document mDoc = null;
+
+    @Before
+    public void setUp() throws Exception {
+        System.out.println("set up.");
+        mDoc = TestUtils.document();
+    }
+
+    /** Test for android safety label missing version. */
+    @Test
+    public void testAndroidSafetyLabelMissingVersion() throws Exception {
+        System.out.println("starting testAndroidSafetyLabelMissingVersion.");
+        hrToOdExpectException(MISSING_VERSION_FILE_NAME);
+    }
+
+    /** Test for android safety label valid empty. */
+    @Test
+    public void testAndroidSafetyLabelValidEmptyFile() throws Exception {
+        System.out.println("starting testAndroidSafetyLabelValidEmptyFile.");
+        testHrToOdAndroidSafetyLabel(VALID_EMPTY_FILE_NAME);
+    }
+
+    /** Test for android safety label with safety labels. */
+    @Test
+    public void testAndroidSafetyLabelWithSafetyLabels() throws Exception {
+        System.out.println("starting testAndroidSafetyLabelWithSafetyLabels.");
+        testHrToOdAndroidSafetyLabel(WITH_SAFETY_LABELS_FILE_NAME);
+    }
+
+    /** Test for android safety label with system app safety label. */
+    @Test
+    public void testAndroidSafetyLabelWithSystemAppSafetyLabel() throws Exception {
+        System.out.println("starting testAndroidSafetyLabelWithSystemAppSafetyLabel.");
+        testHrToOdAndroidSafetyLabel(WITH_SYSTEM_APP_SAFETY_LABEL_FILE_NAME);
+    }
+
+    /** Test for android safety label with transparency info. */
+    @Test
+    public void testAndroidSafetyLabelWithTransparencyInfo() throws Exception {
+        System.out.println("starting testAndroidSafetyLabelWithTransparencyInfo.");
+        testHrToOdAndroidSafetyLabel(WITH_TRANSPARENCY_INFO_FILE_NAME);
+    }
+
+    private void hrToOdExpectException(String fileName) {
+        TestUtils.hrToOdExpectException(
+                new AndroidSafetyLabelFactory(), ANDROID_SAFETY_LABEL_HR_PATH, fileName);
+    }
+
+    private void testHrToOdAndroidSafetyLabel(String fileName) throws Exception {
+        TestUtils.testHrToOd(
+                mDoc,
+                new AndroidSafetyLabelFactory(),
+                ANDROID_SAFETY_LABEL_HR_PATH,
+                ANDROID_SAFETY_LABEL_OD_PATH,
+                fileName);
+    }
+}
diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AppInfoTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AppInfoTest.java
new file mode 100644
index 0000000..a015e2e
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AppInfoTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib.marshallable;
+
+import static org.junit.Assert.assertThrows;
+
+import com.android.asllib.testutils.TestUtils;
+import com.android.asllib.util.MalformedXmlException;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.w3c.dom.Document;
+
+import java.nio.file.Paths;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class AppInfoTest {
+    private static final String APP_INFO_HR_PATH = "com/android/asllib/appinfo/hr";
+    private static final String APP_INFO_OD_PATH = "com/android/asllib/appinfo/od";
+    public static final List<String> REQUIRED_FIELD_NAMES =
+            List.of(
+                    "title",
+                    "description",
+                    "containsAds",
+                    "obeyAps",
+                    "adsFingerprinting",
+                    "securityFingerprinting",
+                    "privacyPolicy",
+                    "securityEndpoints",
+                    "firstPartyEndpoints",
+                    "serviceProviderEndpoints",
+                    "category",
+                    "email");
+    public static final List<String> OPTIONAL_FIELD_NAMES = List.of("website");
+
+    private static final String ALL_FIELDS_VALID_FILE_NAME = "all-fields-valid.xml";
+
+    private Document mDoc = null;
+
+    /** Logic for setting up tests (empty if not yet needed). */
+    public static void main(String[] params) throws Exception {}
+
+    @Before
+    public void setUp() throws Exception {
+        System.out.println("set up.");
+        mDoc = TestUtils.document();
+    }
+
+    /** Test for all fields valid. */
+    @Test
+    public void testAllFieldsValid() throws Exception {
+        System.out.println("starting testAllFieldsValid.");
+        testHrToOdAppInfo(ALL_FIELDS_VALID_FILE_NAME);
+    }
+
+    /** Tests missing required fields fails. */
+    @Test
+    public void testMissingRequiredFields() throws Exception {
+        System.out.println("Starting testMissingRequiredFields");
+        for (String reqField : REQUIRED_FIELD_NAMES) {
+            System.out.println("testing missing required field: " + reqField);
+            var appInfoEle =
+                    TestUtils.getElementsFromResource(
+                            Paths.get(APP_INFO_HR_PATH, ALL_FIELDS_VALID_FILE_NAME));
+            appInfoEle.get(0).removeAttribute(reqField);
+
+            assertThrows(
+                    MalformedXmlException.class,
+                    () -> new AppInfoFactory().createFromHrElements(appInfoEle));
+        }
+    }
+
+    /** Tests missing optional fields passes. */
+    @Test
+    public void testMissingOptionalFields() throws Exception {
+        for (String optField : OPTIONAL_FIELD_NAMES) {
+            var ele =
+                    TestUtils.getElementsFromResource(
+                            Paths.get(APP_INFO_HR_PATH, ALL_FIELDS_VALID_FILE_NAME));
+            ele.get(0).removeAttribute(optField);
+            AppInfo appInfo = new AppInfoFactory().createFromHrElements(ele);
+            appInfo.toOdDomElements(mDoc);
+        }
+    }
+
+    private void testHrToOdAppInfo(String fileName) throws Exception {
+        TestUtils.testHrToOd(
+                mDoc, new AppInfoFactory(), APP_INFO_HR_PATH, APP_INFO_OD_PATH, fileName);
+    }
+}
diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataCategoryTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataCategoryTest.java
new file mode 100644
index 0000000..822f175
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataCategoryTest.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib.marshallable;
+
+import com.android.asllib.testutils.TestUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.w3c.dom.Document;
+
+@RunWith(JUnit4.class)
+public class DataCategoryTest {
+    private static final String DATA_CATEGORY_HR_PATH = "com/android/asllib/datacategory/hr";
+    private static final String DATA_CATEGORY_OD_PATH = "com/android/asllib/datacategory/od";
+
+    private static final String VALID_PERSONAL_FILE_NAME = "data-category-personal.xml";
+    private static final String VALID_PARTIAL_PERSONAL_FILE_NAME =
+            "data-category-personal-partial.xml";
+    private static final String VALID_FINANCIAL_FILE_NAME = "data-category-financial.xml";
+    private static final String VALID_LOCATION_FILE_NAME = "data-category-location.xml";
+    private static final String VALID_EMAIL_TEXT_MESSAGE_FILE_NAME =
+            "data-category-email-text-message.xml";
+    private static final String VALID_PHOTO_VIDEO_FILE_NAME = "data-category-photo-video.xml";
+    private static final String VALID_AUDIO_FILE_NAME = "data-category-audio.xml";
+    private static final String VALID_STORAGE_FILE_NAME = "data-category-storage.xml";
+    private static final String VALID_HEALTH_FITNESS_FILE_NAME = "data-category-health-fitness.xml";
+    private static final String VALID_CONTACTS_FILE_NAME = "data-category-contacts.xml";
+    private static final String VALID_CALENDAR_FILE_NAME = "data-category-calendar.xml";
+    private static final String VALID_IDENTIFIERS_FILE_NAME = "data-category-identifiers.xml";
+    private static final String VALID_APP_PERFORMANCE_FILE_NAME =
+            "data-category-app-performance.xml";
+    private static final String VALID_ACTIONS_IN_APP_FILE_NAME = "data-category-actions-in-app.xml";
+    private static final String VALID_SEARCH_AND_BROWSING_FILE_NAME =
+            "data-category-search-and-browsing.xml";
+
+    private static final String EMPTY_PURPOSE_PERSONAL_FILE_NAME =
+            "data-category-personal-empty-purpose.xml";
+    private static final String MISSING_PURPOSE_PERSONAL_FILE_NAME =
+            "data-category-personal-missing-purpose.xml";
+    private static final String UNRECOGNIZED_TYPE_PERSONAL_FILE_NAME =
+            "data-category-personal-unrecognized-type.xml";
+    private static final String UNRECOGNIZED_CATEGORY_FILE_NAME = "data-category-unrecognized.xml";
+
+    private Document mDoc = null;
+
+    /** Logic for setting up tests (empty if not yet needed). */
+    public static void main(String[] params) throws Exception {}
+
+    @Before
+    public void setUp() throws Exception {
+        System.out.println("set up.");
+        mDoc = TestUtils.document();
+    }
+
+    /** Test for data category personal. */
+    @Test
+    public void testDataCategoryPersonal() throws Exception {
+        System.out.println("starting testDataCategoryPersonal.");
+        testHrToOdDataCategory(VALID_PERSONAL_FILE_NAME);
+    }
+
+    /** Test for data category financial. */
+    @Test
+    public void testDataCategoryFinancial() throws Exception {
+        System.out.println("starting testDataCategoryFinancial.");
+        testHrToOdDataCategory(VALID_FINANCIAL_FILE_NAME);
+    }
+
+    /** Test for data category location. */
+    @Test
+    public void testDataCategoryLocation() throws Exception {
+        System.out.println("starting testDataCategoryLocation.");
+        testHrToOdDataCategory(VALID_LOCATION_FILE_NAME);
+    }
+
+    /** Test for data category email text message. */
+    @Test
+    public void testDataCategoryEmailTextMessage() throws Exception {
+        System.out.println("starting testDataCategoryEmailTextMessage.");
+        testHrToOdDataCategory(VALID_EMAIL_TEXT_MESSAGE_FILE_NAME);
+    }
+
+    /** Test for data category photo video. */
+    @Test
+    public void testDataCategoryPhotoVideo() throws Exception {
+        System.out.println("starting testDataCategoryPhotoVideo.");
+        testHrToOdDataCategory(VALID_PHOTO_VIDEO_FILE_NAME);
+    }
+
+    /** Test for data category audio. */
+    @Test
+    public void testDataCategoryAudio() throws Exception {
+        System.out.println("starting testDataCategoryAudio.");
+        testHrToOdDataCategory(VALID_AUDIO_FILE_NAME);
+    }
+
+    /** Test for data category storage. */
+    @Test
+    public void testDataCategoryStorage() throws Exception {
+        System.out.println("starting testDataCategoryStorage.");
+        testHrToOdDataCategory(VALID_STORAGE_FILE_NAME);
+    }
+
+    /** Test for data category health fitness. */
+    @Test
+    public void testDataCategoryHealthFitness() throws Exception {
+        System.out.println("starting testDataCategoryHealthFitness.");
+        testHrToOdDataCategory(VALID_HEALTH_FITNESS_FILE_NAME);
+    }
+
+    /** Test for data category contacts. */
+    @Test
+    public void testDataCategoryContacts() throws Exception {
+        System.out.println("starting testDataCategoryContacts.");
+        testHrToOdDataCategory(VALID_CONTACTS_FILE_NAME);
+    }
+
+    /** Test for data category calendar. */
+    @Test
+    public void testDataCategoryCalendar() throws Exception {
+        System.out.println("starting testDataCategoryCalendar.");
+        testHrToOdDataCategory(VALID_CALENDAR_FILE_NAME);
+    }
+
+    /** Test for data category identifiers. */
+    @Test
+    public void testDataCategoryIdentifiers() throws Exception {
+        System.out.println("starting testDataCategoryIdentifiers.");
+        testHrToOdDataCategory(VALID_IDENTIFIERS_FILE_NAME);
+    }
+
+    /** Test for data category app performance. */
+    @Test
+    public void testDataCategoryAppPerformance() throws Exception {
+        System.out.println("starting testDataCategoryAppPerformance.");
+        testHrToOdDataCategory(VALID_APP_PERFORMANCE_FILE_NAME);
+    }
+
+    /** Test for data category actions in app. */
+    @Test
+    public void testDataCategoryActionsInApp() throws Exception {
+        System.out.println("starting testDataCategoryActionsInApp.");
+        testHrToOdDataCategory(VALID_ACTIONS_IN_APP_FILE_NAME);
+    }
+
+    /** Test for data category search and browsing. */
+    @Test
+    public void testDataCategorySearchAndBrowsing() throws Exception {
+        System.out.println("starting testDataCategorySearchAndBrowsing.");
+        testHrToOdDataCategory(VALID_SEARCH_AND_BROWSING_FILE_NAME);
+    }
+
+    /** Test for data category search and browsing. */
+    @Test
+    public void testMissingOptionalsAllowed() throws Exception {
+        System.out.println("starting testMissingOptionalsAllowed.");
+        testHrToOdDataCategory(VALID_PARTIAL_PERSONAL_FILE_NAME);
+    }
+
+    /** Test for empty purposes. */
+    @Test
+    public void testEmptyPurposesNotAllowed() throws Exception {
+        System.out.println("starting testEmptyPurposesNotAllowed.");
+        hrToOdExpectException(EMPTY_PURPOSE_PERSONAL_FILE_NAME);
+    }
+
+    /** Test for missing purposes. */
+    @Test
+    public void testMissingPurposesNotAllowed() throws Exception {
+        System.out.println("starting testMissingPurposesNotAllowed.");
+        hrToOdExpectException(MISSING_PURPOSE_PERSONAL_FILE_NAME);
+    }
+
+    /** Test for unrecognized type. */
+    @Test
+    public void testUnrecognizedTypeNotAllowed() throws Exception {
+        System.out.println("starting testUnrecognizedTypeNotAllowed.");
+        hrToOdExpectException(UNRECOGNIZED_TYPE_PERSONAL_FILE_NAME);
+    }
+
+    /** Test for unrecognized category. */
+    @Test
+    public void testUnrecognizedCategoryNotAllowed() throws Exception {
+        System.out.println("starting testUnrecognizedCategoryNotAllowed.");
+        hrToOdExpectException(UNRECOGNIZED_CATEGORY_FILE_NAME);
+    }
+
+    private void hrToOdExpectException(String fileName) {
+        TestUtils.hrToOdExpectException(new DataCategoryFactory(), DATA_CATEGORY_HR_PATH, fileName);
+    }
+
+    private void testHrToOdDataCategory(String fileName) throws Exception {
+        TestUtils.testHrToOd(
+                mDoc,
+                new DataCategoryFactory(),
+                DATA_CATEGORY_HR_PATH,
+                DATA_CATEGORY_OD_PATH,
+                fileName);
+    }
+}
diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataLabelsTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataLabelsTest.java
new file mode 100644
index 0000000..2be447e
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataLabelsTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib.marshallable;
+
+import com.android.asllib.testutils.TestUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.w3c.dom.Document;
+
+@RunWith(JUnit4.class)
+public class DataLabelsTest {
+    private static final String DATA_LABELS_HR_PATH = "com/android/asllib/datalabels/hr";
+    private static final String DATA_LABELS_OD_PATH = "com/android/asllib/datalabels/od";
+
+    private static final String ACCESSED_VALID_BOOL_FILE_NAME =
+            "data-labels-accessed-valid-bool.xml";
+    private static final String ACCESSED_INVALID_BOOL_FILE_NAME =
+            "data-labels-accessed-invalid-bool.xml";
+    private static final String COLLECTED_VALID_BOOL_FILE_NAME =
+            "data-labels-collected-valid-bool.xml";
+    private static final String COLLECTED_INVALID_BOOL_FILE_NAME =
+            "data-labels-collected-invalid-bool.xml";
+    private static final String SHARED_VALID_BOOL_FILE_NAME = "data-labels-shared-valid-bool.xml";
+    private static final String SHARED_INVALID_BOOL_FILE_NAME =
+            "data-labels-shared-invalid-bool.xml";
+
+    private Document mDoc = null;
+
+    @Before
+    public void setUp() throws Exception {
+        System.out.println("set up.");
+        mDoc = TestUtils.document();
+    }
+
+    /** Test for data labels accessed valid bool. */
+    @Test
+    public void testDataLabelsAccessedValidBool() throws Exception {
+        System.out.println("starting testDataLabelsAccessedValidBool.");
+        testHrToOdDataLabels(ACCESSED_VALID_BOOL_FILE_NAME);
+    }
+
+    /** Test for data labels accessed invalid bool. */
+    @Test
+    public void testDataLabelsAccessedInvalidBool() throws Exception {
+        System.out.println("starting testDataLabelsAccessedInvalidBool.");
+        hrToOdExpectException(ACCESSED_INVALID_BOOL_FILE_NAME);
+    }
+
+    /** Test for data labels collected valid bool. */
+    @Test
+    public void testDataLabelsCollectedValidBool() throws Exception {
+        System.out.println("starting testDataLabelsCollectedValidBool.");
+        testHrToOdDataLabels(COLLECTED_VALID_BOOL_FILE_NAME);
+    }
+
+    /** Test for data labels collected invalid bool. */
+    @Test
+    public void testDataLabelsCollectedInvalidBool() throws Exception {
+        System.out.println("starting testDataLabelsCollectedInvalidBool.");
+        hrToOdExpectException(COLLECTED_INVALID_BOOL_FILE_NAME);
+    }
+
+    /** Test for data labels shared valid bool. */
+    @Test
+    public void testDataLabelsSharedValidBool() throws Exception {
+        System.out.println("starting testDataLabelsSharedValidBool.");
+        testHrToOdDataLabels(SHARED_VALID_BOOL_FILE_NAME);
+    }
+
+    /** Test for data labels shared invalid bool. */
+    @Test
+    public void testDataLabelsSharedInvalidBool() throws Exception {
+        System.out.println("starting testDataLabelsSharedInvalidBool.");
+        hrToOdExpectException(SHARED_INVALID_BOOL_FILE_NAME);
+    }
+
+    private void hrToOdExpectException(String fileName) {
+        TestUtils.hrToOdExpectException(new DataLabelsFactory(), DATA_LABELS_HR_PATH, fileName);
+    }
+
+    private void testHrToOdDataLabels(String fileName) throws Exception {
+        TestUtils.testHrToOd(
+                mDoc, new DataLabelsFactory(), DATA_LABELS_HR_PATH, DATA_LABELS_OD_PATH, fileName);
+    }
+}
diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DeveloperInfoTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DeveloperInfoTest.java
new file mode 100644
index 0000000..ff8346a
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DeveloperInfoTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib.marshallable;
+
+import static org.junit.Assert.assertThrows;
+
+import com.android.asllib.testutils.TestUtils;
+import com.android.asllib.util.MalformedXmlException;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.w3c.dom.Document;
+
+import java.nio.file.Paths;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class DeveloperInfoTest {
+    private static final String DEVELOPER_INFO_HR_PATH = "com/android/asllib/developerinfo/hr";
+    private static final String DEVELOPER_INFO_OD_PATH = "com/android/asllib/developerinfo/od";
+    public static final List<String> REQUIRED_FIELD_NAMES =
+            List.of("address", "countryRegion", "email", "name", "relationship");
+    public static final List<String> OPTIONAL_FIELD_NAMES = List.of("website", "registryId");
+
+    private static final String ALL_FIELDS_VALID_FILE_NAME = "all-fields-valid.xml";
+
+    private Document mDoc = null;
+
+    /** Logic for setting up tests (empty if not yet needed). */
+    public static void main(String[] params) throws Exception {}
+
+    @Before
+    public void setUp() throws Exception {
+        System.out.println("set up.");
+        mDoc = TestUtils.document();
+    }
+
+    /** Test for all fields valid. */
+    @Test
+    public void testAllFieldsValid() throws Exception {
+        System.out.println("starting testAllFieldsValid.");
+        testHrToOdDeveloperInfo(ALL_FIELDS_VALID_FILE_NAME);
+    }
+
+    /** Tests missing required fields fails. */
+    @Test
+    public void testMissingRequiredFields() throws Exception {
+        System.out.println("Starting testMissingRequiredFields");
+        for (String reqField : REQUIRED_FIELD_NAMES) {
+            System.out.println("testing missing required field: " + reqField);
+            var developerInfoEle =
+                    TestUtils.getElementsFromResource(
+                            Paths.get(DEVELOPER_INFO_HR_PATH, ALL_FIELDS_VALID_FILE_NAME));
+            developerInfoEle.get(0).removeAttribute(reqField);
+
+            assertThrows(
+                    MalformedXmlException.class,
+                    () -> new DeveloperInfoFactory().createFromHrElements(developerInfoEle));
+        }
+    }
+
+    /** Tests missing optional fields passes. */
+    @Test
+    public void testMissingOptionalFields() throws Exception {
+        for (String optField : OPTIONAL_FIELD_NAMES) {
+            var developerInfoEle =
+                    TestUtils.getElementsFromResource(
+                            Paths.get(DEVELOPER_INFO_HR_PATH, ALL_FIELDS_VALID_FILE_NAME));
+            developerInfoEle.get(0).removeAttribute(optField);
+            DeveloperInfo developerInfo =
+                    new DeveloperInfoFactory().createFromHrElements(developerInfoEle);
+            developerInfo.toOdDomElements(mDoc);
+        }
+    }
+
+    private void testHrToOdDeveloperInfo(String fileName) throws Exception {
+        TestUtils.testHrToOd(
+                mDoc,
+                new DeveloperInfoFactory(),
+                DEVELOPER_INFO_HR_PATH,
+                DEVELOPER_INFO_OD_PATH,
+                fileName);
+    }
+}
diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SafetyLabelsTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SafetyLabelsTest.java
new file mode 100644
index 0000000..b62620e
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SafetyLabelsTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib.marshallable;
+
+import com.android.asllib.testutils.TestUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.w3c.dom.Document;
+
+@RunWith(JUnit4.class)
+public class SafetyLabelsTest {
+    private static final String SAFETY_LABELS_HR_PATH = "com/android/asllib/safetylabels/hr";
+    private static final String SAFETY_LABELS_OD_PATH = "com/android/asllib/safetylabels/od";
+
+    private static final String MISSING_VERSION_FILE_NAME = "missing-version.xml";
+    private static final String VALID_EMPTY_FILE_NAME = "valid-empty.xml";
+    private static final String WITH_DATA_LABELS_FILE_NAME = "with-data-labels.xml";
+
+    private Document mDoc = null;
+
+    @Before
+    public void setUp() throws Exception {
+        System.out.println("set up.");
+        mDoc = TestUtils.document();
+    }
+
+    /** Test for safety labels missing version. */
+    @Test
+    public void testSafetyLabelsMissingVersion() throws Exception {
+        System.out.println("starting testSafetyLabelsMissingVersion.");
+        hrToOdExpectException(MISSING_VERSION_FILE_NAME);
+    }
+
+    /** Test for safety labels valid empty. */
+    @Test
+    public void testSafetyLabelsValidEmptyFile() throws Exception {
+        System.out.println("starting testSafetyLabelsValidEmptyFile.");
+        testHrToOdSafetyLabels(VALID_EMPTY_FILE_NAME);
+    }
+
+    /** Test for safety labels with data labels. */
+    @Test
+    public void testSafetyLabelsWithDataLabels() throws Exception {
+        System.out.println("starting testSafetyLabelsWithDataLabels.");
+        testHrToOdSafetyLabels(WITH_DATA_LABELS_FILE_NAME);
+    }
+
+    private void hrToOdExpectException(String fileName) {
+        TestUtils.hrToOdExpectException(new SafetyLabelsFactory(), SAFETY_LABELS_HR_PATH, fileName);
+    }
+
+    private void testHrToOdSafetyLabels(String fileName) throws Exception {
+        TestUtils.testHrToOd(
+                mDoc,
+                new SafetyLabelsFactory(),
+                SAFETY_LABELS_HR_PATH,
+                SAFETY_LABELS_OD_PATH,
+                fileName);
+    }
+}
diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SystemAppSafetyLabelTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SystemAppSafetyLabelTest.java
new file mode 100644
index 0000000..191091a
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SystemAppSafetyLabelTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib.marshallable;
+
+import com.android.asllib.testutils.TestUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.w3c.dom.Document;
+
+@RunWith(JUnit4.class)
+public class SystemAppSafetyLabelTest {
+    private static final String SYSTEM_APP_SAFETY_LABEL_HR_PATH =
+            "com/android/asllib/systemappsafetylabel/hr";
+    private static final String SYSTEM_APP_SAFETY_LABEL_OD_PATH =
+            "com/android/asllib/systemappsafetylabel/od";
+
+    private static final String VALID_FILE_NAME = "valid.xml";
+    private static final String MISSING_URL_FILE_NAME = "missing-url.xml";
+
+    private Document mDoc = null;
+
+    /** Logic for setting up tests (empty if not yet needed). */
+    public static void main(String[] params) throws Exception {}
+
+    @Before
+    public void setUp() throws Exception {
+        System.out.println("set up.");
+        mDoc = TestUtils.document();
+    }
+
+    /** Test for valid. */
+    @Test
+    public void testValid() throws Exception {
+        System.out.println("starting testValid.");
+        testHrToOdSystemAppSafetyLabel(VALID_FILE_NAME);
+    }
+
+    /** Tests missing url. */
+    @Test
+    public void testMissingUrl() throws Exception {
+        System.out.println("starting testMissingUrl.");
+        hrToOdExpectException(MISSING_URL_FILE_NAME);
+    }
+
+    private void hrToOdExpectException(String fileName) {
+        TestUtils.hrToOdExpectException(
+                new SystemAppSafetyLabelFactory(), SYSTEM_APP_SAFETY_LABEL_HR_PATH, fileName);
+    }
+
+    private void testHrToOdSystemAppSafetyLabel(String fileName) throws Exception {
+        TestUtils.testHrToOd(
+                mDoc,
+                new SystemAppSafetyLabelFactory(),
+                SYSTEM_APP_SAFETY_LABEL_HR_PATH,
+                SYSTEM_APP_SAFETY_LABEL_OD_PATH,
+                fileName);
+    }
+}
diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/TransparencyInfoTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/TransparencyInfoTest.java
new file mode 100644
index 0000000..56503f7
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/TransparencyInfoTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib.marshallable;
+
+import com.android.asllib.testutils.TestUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.w3c.dom.Document;
+
+@RunWith(JUnit4.class)
+public class TransparencyInfoTest {
+    private static final String TRANSPARENCY_INFO_HR_PATH =
+            "com/android/asllib/transparencyinfo/hr";
+    private static final String TRANSPARENCY_INFO_OD_PATH =
+            "com/android/asllib/transparencyinfo/od";
+
+    private static final String VALID_EMPTY_FILE_NAME = "valid-empty.xml";
+    private static final String WITH_DEVELOPER_INFO_FILE_NAME = "with-developer-info.xml";
+    private static final String WITH_APP_INFO_FILE_NAME = "with-app-info.xml";
+
+    private Document mDoc = null;
+
+    @Before
+    public void setUp() throws Exception {
+        System.out.println("set up.");
+        mDoc = TestUtils.document();
+    }
+
+    /** Test for transparency info valid empty. */
+    @Test
+    public void testTransparencyInfoValidEmptyFile() throws Exception {
+        System.out.println("starting testTransparencyInfoValidEmptyFile.");
+        testHrToOdTransparencyInfo(VALID_EMPTY_FILE_NAME);
+    }
+
+    /** Test for transparency info with developer info. */
+    @Test
+    public void testTransparencyInfoWithDeveloperInfo() throws Exception {
+        System.out.println("starting testTransparencyInfoWithDeveloperInfo.");
+        testHrToOdTransparencyInfo(WITH_DEVELOPER_INFO_FILE_NAME);
+    }
+
+    /** Test for transparency info with app info. */
+    @Test
+    public void testTransparencyInfoWithAppInfo() throws Exception {
+        System.out.println("starting testTransparencyInfoWithAppInfo.");
+        testHrToOdTransparencyInfo(WITH_APP_INFO_FILE_NAME);
+    }
+
+    private void testHrToOdTransparencyInfo(String fileName) throws Exception {
+        TestUtils.testHrToOd(
+                mDoc,
+                new TransparencyInfoFactory(),
+                TRANSPARENCY_INFO_HR_PATH,
+                TRANSPARENCY_INFO_OD_PATH,
+                fileName);
+    }
+}
diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/testutils/TestUtils.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/testutils/TestUtils.java
new file mode 100644
index 0000000..faea340
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/testutils/TestUtils.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.asllib.testutils;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import com.android.asllib.marshallable.AslMarshallable;
+import com.android.asllib.marshallable.AslMarshallableFactory;
+import com.android.asllib.util.MalformedXmlException;
+import com.android.asllib.util.XmlUtils;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.SAXException;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+
+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 TestUtils {
+    public static final String HOLDER_TAG_NAME = "holder_of_flattened_for_testing";
+
+    /** Reads a Resource file into a String. */
+    public static String readStrFromResource(Path filePath) throws IOException {
+        InputStream hrStream =
+                TestUtils.class.getClassLoader().getResourceAsStream(filePath.toString());
+        return new String(hrStream.readAllBytes(), StandardCharsets.UTF_8);
+    }
+
+    /** Gets List of Element from a path to an existing Resource. */
+    public static List<Element> getElementsFromResource(Path filePath)
+            throws ParserConfigurationException, IOException, SAXException {
+        String str = readStrFromResource(filePath);
+        InputStream stream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
+
+        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        factory.setNamespaceAware(true);
+        Document document = factory.newDocumentBuilder().parse(stream);
+        Element root = document.getDocumentElement();
+        if (root.getTagName().equals(HOLDER_TAG_NAME)) {
+            String tagName =
+                    XmlUtils.asElementList(root.getChildNodes()).stream()
+                            .findFirst()
+                            .get()
+                            .getTagName();
+            return XmlUtils.asElementList(root.getElementsByTagName(tagName));
+        } else {
+            return List.of(root);
+        }
+    }
+
+    /** Reads a Document into a String. */
+    public static String docToStr(Document doc, boolean omitXmlDeclaration)
+            throws TransformerException {
+        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, omitXmlDeclaration ? "yes" : "no");
+
+        ByteArrayOutputStream outStream = new ByteArrayOutputStream();
+        StreamResult streamResult = new StreamResult(outStream); // out
+        DOMSource domSource = new DOMSource(doc);
+        transformer.transform(domSource, streamResult);
+
+        return outStream.toString(StandardCharsets.UTF_8);
+    }
+
+    /**
+     * Gets formatted XML for slightly more robust comparison checking than naive string comparison.
+     */
+    public static String getFormattedXml(String xmlStr, boolean omitXmlDeclaration)
+            throws ParserConfigurationException, IOException, SAXException, TransformerException {
+        InputStream stream = new ByteArrayInputStream(xmlStr.getBytes(StandardCharsets.UTF_8));
+        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        factory.setNamespaceAware(true);
+        Document document = factory.newDocumentBuilder().parse(stream);
+
+        return docToStr(document, omitXmlDeclaration);
+    }
+
+    /** Helper for getting a new Document */
+    public static Document document() throws ParserConfigurationException {
+        return DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
+    }
+
+    /** Helper for testing human-readable to on-device conversion expecting exception */
+    public static <T extends AslMarshallable> void hrToOdExpectException(
+            AslMarshallableFactory<T> factory, String hrFolderPath, String fileName) {
+        assertThrows(
+                MalformedXmlException.class,
+                () -> {
+                    factory.createFromHrElements(
+                            TestUtils.getElementsFromResource(Paths.get(hrFolderPath, fileName)));
+                });
+    }
+
+    /** Helper for testing human-readable to on-device conversion */
+    public static <T extends AslMarshallable> void testHrToOd(
+            Document doc,
+            AslMarshallableFactory<T> factory,
+            String hrFolderPath,
+            String odFolderPath,
+            String fileName)
+            throws Exception {
+        AslMarshallable marshallable =
+                factory.createFromHrElements(
+                        TestUtils.getElementsFromResource(Paths.get(hrFolderPath, fileName)));
+
+        for (var child : marshallable.toOdDomElements(doc)) {
+            doc.appendChild(child);
+        }
+        String converted = TestUtils.docToStr(doc, true);
+        System.out.println("converted: " + converted);
+
+        String expectedOdContents =
+                TestUtils.readStrFromResource(Paths.get(odFolderPath, fileName));
+        assertEquals(
+                TestUtils.getFormattedXml(expectedOdContents, true),
+                TestUtils.getFormattedXml(converted, true));
+    }
+}
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/missing-version.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/missing-version.xml
new file mode 100644
index 0000000..ec0cd70
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/missing-version.xml
@@ -0,0 +1,3 @@
+<app-metadata-bundles>
+
+</app-metadata-bundles>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/valid-empty.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/valid-empty.xml
new file mode 100644
index 0000000..19bfd82
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/valid-empty.xml
@@ -0,0 +1 @@
+<app-metadata-bundles version="123456"></app-metadata-bundles>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-safety-labels.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-safety-labels.xml
new file mode 100644
index 0000000..53794a1
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-safety-labels.xml
@@ -0,0 +1,4 @@
+<app-metadata-bundles version="123456">
+    <safety-labels version="12345">
+    </safety-labels>
+</app-metadata-bundles>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-system-app-safety-label.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-system-app-safety-label.xml
new file mode 100644
index 0000000..7bcde45
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-system-app-safety-label.xml
@@ -0,0 +1,4 @@
+<app-metadata-bundles version="123456">
+<system-app-safety-label url="www.example.com">
+</system-app-safety-label>
+</app-metadata-bundles>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-transparency-info.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-transparency-info.xml
new file mode 100644
index 0000000..00bcfa8
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-transparency-info.xml
@@ -0,0 +1,4 @@
+<app-metadata-bundles version="123456">
+<transparency-info>
+</transparency-info>
+</app-metadata-bundles>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/valid-empty.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/valid-empty.xml
new file mode 100644
index 0000000..37bdfad
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/valid-empty.xml
@@ -0,0 +1,3 @@
+<bundle>
+    <long name="version" value="123456"/>
+</bundle>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-safety-labels.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-safety-labels.xml
new file mode 100644
index 0000000..74644ed
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-safety-labels.xml
@@ -0,0 +1,6 @@
+<bundle>
+    <long name="version" value="123456"/>
+    <pbundle_as_map name="safety_labels">
+        <long name="version" value="12345"/>
+    </pbundle_as_map>
+</bundle>
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-system-app-safety-label.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-system-app-safety-label.xml
new file mode 100644
index 0000000..ef0f549
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-system-app-safety-label.xml
@@ -0,0 +1,6 @@
+<bundle>
+    <long name="version" value="123456"/>
+    <pbundle_as_map name="system_app_safety_label">
+        <string name="url" value="www.example.com"/>
+    </pbundle_as_map>
+</bundle>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-transparency-info.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-transparency-info.xml
new file mode 100644
index 0000000..63c5094
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-transparency-info.xml
@@ -0,0 +1,4 @@
+<bundle>
+    <long name="version" value="123456"/>
+    <pbundle_as_map name="transparency_info"/>
+</bundle>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/hr/all-fields-valid.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/hr/all-fields-valid.xml
new file mode 100644
index 0000000..883170a2
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/hr/all-fields-valid.xml
@@ -0,0 +1,14 @@
+<app-info
+    title="beervision"
+    description="a beer app"
+    containsAds="true"
+    obeyAps="false"
+    adsFingerprinting="false"
+    securityFingerprinting="false"
+    privacyPolicy="www.example.com"
+    securityEndpoints="url1|url2|url3"
+    firstPartyEndpoints="url1"
+    serviceProviderEndpoints="url55|url56"
+    category="Food and drink"
+    email="max@maxloh.com"
+    website="www.example.com" />
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/all-fields-valid.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/all-fields-valid.xml
new file mode 100644
index 0000000..6e976a3
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/all-fields-valid.xml
@@ -0,0 +1,25 @@
+
+<pbundle_as_map name="app_info">
+    <string name="title" value="beervision"/>
+    <string name="description" value="a beer app"/>
+    <boolean name="contains_ads" value="true"/>
+    <boolean name="obey_aps" value="false"/>
+    <boolean name="ads_fingerprinting" value="false"/>
+    <boolean name="security_fingerprinting" value="false"/>
+    <string name="privacy_policy" value="www.example.com"/>
+    <string-array name="security_endpoint" num="3">
+        <item value="url1"/>
+        <item value="url2"/>
+        <item value="url3"/>
+    </string-array>
+    <string-array name="first_party_endpoint" num="1">
+        <item value="url1"/>
+    </string-array>
+    <string-array name="service_provider_endpoint" num="2">
+        <item value="url55"/>
+        <item value="url56"/>
+    </string-array>
+    <string name="category" value="Food and drink"/>
+    <string name="email" value="max@maxloh.com"/>
+    <string name="website" value="www.example.com"/>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-actions-in-app.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-actions-in-app.xml
new file mode 100644
index 0000000..520e525
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-actions-in-app.xml
@@ -0,0 +1,17 @@
+<holder_of_flattened_for_testing>
+    <data-shared dataCategory="actions_in_app"
+        dataType="user_interaction"
+        purposes="analytics" />
+    <data-shared dataCategory="actions_in_app"
+        dataType="in_app_search_history"
+        purposes="analytics" />
+    <data-shared dataCategory="actions_in_app"
+        dataType="installed_apps"
+        purposes="analytics" />
+    <data-shared dataCategory="actions_in_app"
+        dataType="user_generated_content"
+        purposes="analytics" />
+    <data-shared dataCategory="actions_in_app"
+        dataType="other"
+        purposes="analytics" />
+</holder_of_flattened_for_testing>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-app-performance.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-app-performance.xml
new file mode 100644
index 0000000..0d08e5b
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-app-performance.xml
@@ -0,0 +1,11 @@
+<holder_of_flattened_for_testing>
+    <data-shared dataCategory="app_performance"
+        dataType="crash_logs"
+        purposes="analytics" />
+    <data-shared dataCategory="app_performance"
+        dataType="performance_diagnostics"
+        purposes="analytics" />
+    <data-shared dataCategory="app_performance"
+        dataType="other"
+        purposes="analytics" />
+</holder_of_flattened_for_testing>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-audio.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-audio.xml
new file mode 100644
index 0000000..b1cf3b4
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-audio.xml
@@ -0,0 +1,11 @@
+<holder_of_flattened_for_testing>
+    <data-shared dataCategory="audio"
+        dataType="sound_recordings"
+        purposes="analytics" />
+    <data-shared dataCategory="audio"
+        dataType="music_files"
+        purposes="analytics" />
+    <data-shared dataCategory="audio"
+        dataType="other"
+        purposes="analytics" />
+</holder_of_flattened_for_testing>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-calendar.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-calendar.xml
new file mode 100644
index 0000000..a723070
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-calendar.xml
@@ -0,0 +1,5 @@
+<holder_of_flattened_for_testing>
+    <data-shared dataCategory="calendar"
+        dataType="calendar"
+        purposes="analytics" />
+</holder_of_flattened_for_testing>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-contacts.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-contacts.xml
new file mode 100644
index 0000000..2fe28ff
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-contacts.xml
@@ -0,0 +1,5 @@
+<holder_of_flattened_for_testing>
+    <data-shared dataCategory="contacts"
+        dataType="contacts"
+        purposes="analytics" />
+</holder_of_flattened_for_testing>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-email-text-message.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-email-text-message.xml
new file mode 100644
index 0000000..49a326f
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-email-text-message.xml
@@ -0,0 +1,11 @@
+<holder_of_flattened_for_testing>
+    <data-shared dataCategory="email_text_message"
+        dataType="emails"
+        purposes="analytics" />
+    <data-shared dataCategory="email_text_message"
+        dataType="text_messages"
+        purposes="analytics" />
+    <data-shared dataCategory="email_text_message"
+        dataType="other"
+        purposes="analytics" />
+</holder_of_flattened_for_testing>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-financial.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-financial.xml
new file mode 100644
index 0000000..f5de370
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-financial.xml
@@ -0,0 +1,14 @@
+<holder_of_flattened_for_testing>
+    <data-shared dataCategory="financial"
+        dataType="card_bank_account"
+        purposes="analytics" />
+    <data-shared dataCategory="financial"
+    dataType="purchase_history"
+    purposes="analytics" />
+    <data-shared dataCategory="financial"
+        dataType="credit_score"
+        purposes="analytics" />
+    <data-shared dataCategory="financial"
+        dataType="other"
+        purposes="analytics" />
+</holder_of_flattened_for_testing>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-health-fitness.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-health-fitness.xml
new file mode 100644
index 0000000..9891f81
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-health-fitness.xml
@@ -0,0 +1,8 @@
+<holder_of_flattened_for_testing>
+    <data-shared dataCategory="health_fitness"
+        dataType="health"
+        purposes="analytics" />
+    <data-shared dataCategory="health_fitness"
+        dataType="fitness"
+        purposes="analytics" />
+</holder_of_flattened_for_testing>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-identifiers.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-identifiers.xml
new file mode 100644
index 0000000..3e74da1
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-identifiers.xml
@@ -0,0 +1,5 @@
+<holder_of_flattened_for_testing>
+    <data-shared dataCategory="identifiers"
+        dataType="other"
+        purposes="analytics" />
+</holder_of_flattened_for_testing>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-location.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-location.xml
new file mode 100644
index 0000000..4762f16
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-location.xml
@@ -0,0 +1,8 @@
+<holder_of_flattened_for_testing>
+    <data-shared dataCategory="location"
+        dataType="approx_location"
+        purposes="analytics" />
+    <data-shared dataCategory="location"
+        dataType="precise_location"
+        purposes="analytics" />
+</holder_of_flattened_for_testing>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal-empty-purpose.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal-empty-purpose.xml
new file mode 100644
index 0000000..964e178
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal-empty-purpose.xml
@@ -0,0 +1,5 @@
+<holder_of_flattened_for_testing>
+    <data-shared dataCategory="personal"
+    dataType="email_address"
+    purposes="" />
+</holder_of_flattened_for_testing>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal-missing-purpose.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal-missing-purpose.xml
new file mode 100644
index 0000000..3ce1288
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal-missing-purpose.xml
@@ -0,0 +1,4 @@
+<holder_of_flattened_for_testing>
+    <data-shared dataCategory="personal"
+    dataType="email_address" />
+</holder_of_flattened_for_testing>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal-partial.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal-partial.xml
new file mode 100644
index 0000000..68baae3
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal-partial.xml
@@ -0,0 +1,8 @@
+<holder_of_flattened_for_testing>
+    <data-shared dataCategory="personal"
+        dataType="name"
+        purposes="analytics|developer_communications" />
+    <data-shared dataCategory="personal"
+    dataType="email_address"
+    purposes="analytics" />
+</holder_of_flattened_for_testing>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal-unrecognized-type.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal-unrecognized-type.xml
new file mode 100644
index 0000000..921a90a
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal-unrecognized-type.xml
@@ -0,0 +1,5 @@
+<holder_of_flattened_for_testing>
+    <data-shared dataCategory="personal"
+    dataType="unrecognized"
+    purposes="analytics" />
+</holder_of_flattened_for_testing>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal.xml
new file mode 100644
index 0000000..4533773
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal.xml
@@ -0,0 +1,31 @@
+<holder_of_flattened_for_testing>
+    <data-shared dataCategory="personal"
+        dataType="name"
+        ephemeral="true"
+        isCollectionOptional="true"
+        purposes="analytics|developer_communications" />
+    <data-shared dataCategory="personal"
+    dataType="email_address"
+    purposes="analytics" />
+    <data-shared dataCategory="personal"
+    dataType="physical_address"
+    purposes="analytics" />
+    <data-shared dataCategory="personal"
+    dataType="phone_number"
+    purposes="analytics" />
+    <data-shared dataCategory="personal"
+    dataType="race_ethnicity"
+    purposes="analytics" />
+    <data-shared dataCategory="personal"
+    dataType="political_or_religious_beliefs"
+    purposes="analytics" />
+    <data-shared dataCategory="personal"
+    dataType="sexual_orientation_or_gender_identity"
+    purposes="analytics" />
+    <data-shared dataCategory="personal"
+    dataType="personal_identifiers"
+    purposes="analytics" />
+    <data-shared dataCategory="personal"
+    dataType="other"
+    purposes="analytics" />
+</holder_of_flattened_for_testing>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-photo-video.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-photo-video.xml
new file mode 100644
index 0000000..234fb26
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-photo-video.xml
@@ -0,0 +1,8 @@
+<holder_of_flattened_for_testing>
+    <data-shared dataCategory="photo_video"
+        dataType="photos"
+        purposes="analytics" />
+    <data-shared dataCategory="photo_video"
+        dataType="videos"
+        purposes="analytics" />
+</holder_of_flattened_for_testing>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-search-and-browsing.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-search-and-browsing.xml
new file mode 100644
index 0000000..db85163
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-search-and-browsing.xml
@@ -0,0 +1,5 @@
+<holder_of_flattened_for_testing>
+    <data-shared dataCategory="search_and_browsing"
+        dataType="web_browsing_history"
+        purposes="analytics" />
+</holder_of_flattened_for_testing>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-storage.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-storage.xml
new file mode 100644
index 0000000..9aad02d
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-storage.xml
@@ -0,0 +1,5 @@
+<holder_of_flattened_for_testing>
+    <data-shared dataCategory="storage"
+        dataType="files_docs"
+        purposes="analytics" />
+</holder_of_flattened_for_testing>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-unrecognized.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-unrecognized.xml
new file mode 100644
index 0000000..64b9ea7
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-unrecognized.xml
@@ -0,0 +1,5 @@
+<holder_of_flattened_for_testing>
+    <data-shared dataCategory="unrecognized"
+    dataType="email_address"
+    purposes="analytics" />
+</holder_of_flattened_for_testing>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-actions-in-app.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-actions-in-app.xml
new file mode 100644
index 0000000..5b99900
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-actions-in-app.xml
@@ -0,0 +1,27 @@
+<pbundle_as_map name="actions_in_app">
+    <pbundle_as_map name="user_interaction">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+    <pbundle_as_map name="in_app_search_history">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+    <pbundle_as_map name="installed_apps">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+    <pbundle_as_map name="user_generated_content">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+    <pbundle_as_map name="other">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-app-performance.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-app-performance.xml
new file mode 100644
index 0000000..0fe1022
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-app-performance.xml
@@ -0,0 +1,17 @@
+<pbundle_as_map name="app_performance">
+    <pbundle_as_map name="crash_logs">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+    <pbundle_as_map name="performance_diagnostics">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+    <pbundle_as_map name="other">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-audio.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-audio.xml
new file mode 100644
index 0000000..51f1dfd
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-audio.xml
@@ -0,0 +1,17 @@
+<pbundle_as_map name="audio">
+    <pbundle_as_map name="sound_recordings">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+    <pbundle_as_map name="music_files">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+    <pbundle_as_map name="other">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-calendar.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-calendar.xml
new file mode 100644
index 0000000..326da47
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-calendar.xml
@@ -0,0 +1,7 @@
+<pbundle_as_map name="calendar">
+    <pbundle_as_map name="calendar">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-contacts.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-contacts.xml
new file mode 100644
index 0000000..5d4387d
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-contacts.xml
@@ -0,0 +1,7 @@
+<pbundle_as_map name="contacts">
+    <pbundle_as_map name="contacts">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-email-text-message.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-email-text-message.xml
new file mode 100644
index 0000000..5ac98f5
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-email-text-message.xml
@@ -0,0 +1,17 @@
+<pbundle_as_map name="email_text_message">
+    <pbundle_as_map name="emails">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+    <pbundle_as_map name="text_messages">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+    <pbundle_as_map name="other">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-financial.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-financial.xml
new file mode 100644
index 0000000..a66f1a4
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-financial.xml
@@ -0,0 +1,22 @@
+<pbundle_as_map name="financial">
+    <pbundle_as_map name="card_bank_account">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+    <pbundle_as_map name="purchase_history">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+    <pbundle_as_map name="credit_score">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+    <pbundle_as_map name="other">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-health-fitness.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-health-fitness.xml
new file mode 100644
index 0000000..8e697b4
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-health-fitness.xml
@@ -0,0 +1,12 @@
+<pbundle_as_map name="health_fitness">
+    <pbundle_as_map name="health">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+    <pbundle_as_map name="fitness">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-identifiers.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-identifiers.xml
new file mode 100644
index 0000000..34b4016
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-identifiers.xml
@@ -0,0 +1,7 @@
+<pbundle_as_map name="identifiers">
+    <pbundle_as_map name="other">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-location.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-location.xml
new file mode 100644
index 0000000..db2e696
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-location.xml
@@ -0,0 +1,12 @@
+<pbundle_as_map name="location">
+    <pbundle_as_map name="approx_location">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+    <pbundle_as_map name="precise_location">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-personal-partial.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-personal-partial.xml
new file mode 100644
index 0000000..839922a
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-personal-partial.xml
@@ -0,0 +1,13 @@
+<pbundle_as_map name="personal">
+    <pbundle_as_map name="name">
+        <int-array name="purposes" num="2">
+            <item value="2" />
+            <item value="3" />
+        </int-array>
+    </pbundle_as_map>
+    <pbundle_as_map name="email_address">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-personal.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-personal.xml
new file mode 100644
index 0000000..43650b6
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-personal.xml
@@ -0,0 +1,50 @@
+<pbundle_as_map name="personal">
+    <pbundle_as_map name="name">
+        <int-array name="purposes" num="2">
+            <item value="2" />
+            <item value="3" />
+        </int-array>
+        <boolean name="is_collection_optional" value="true" />
+        <boolean name="ephemeral" value="true" />
+    </pbundle_as_map>
+    <pbundle_as_map name="email_address">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+    <pbundle_as_map name="physical_address">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+    <pbundle_as_map name="phone_number">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+    <pbundle_as_map name="race_ethnicity">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+    <pbundle_as_map name="political_or_religious_beliefs">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+    <pbundle_as_map name="sexual_orientation_or_gender_identity">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+    <pbundle_as_map name="personal_identifiers">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+    <pbundle_as_map name="other">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-photo-video.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-photo-video.xml
new file mode 100644
index 0000000..2a31780
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-photo-video.xml
@@ -0,0 +1,12 @@
+<pbundle_as_map name="photo_video">
+    <pbundle_as_map name="photos">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+    <pbundle_as_map name="videos">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-search-and-browsing.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-search-and-browsing.xml
new file mode 100644
index 0000000..9e654ef
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-search-and-browsing.xml
@@ -0,0 +1,7 @@
+<pbundle_as_map name="search_and_browsing">
+    <pbundle_as_map name="web_browsing_history">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-storage.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-storage.xml
new file mode 100644
index 0000000..9abc37f
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-storage.xml
@@ -0,0 +1,7 @@
+<pbundle_as_map name="storage">
+    <pbundle_as_map name="files_docs">
+        <int-array name="purposes" num="1">
+            <item value="2" />
+        </int-array>
+    </pbundle_as_map>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-accessed-invalid-bool.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-accessed-invalid-bool.xml
new file mode 100644
index 0000000..bb45f42
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-accessed-invalid-bool.xml
@@ -0,0 +1,7 @@
+<data-labels>
+    <data-accessed dataCategory="location"
+        dataType="approx_location"
+        ephemeral="false"
+        isSharingOptional="false"
+        purposes="app_functionality" />
+</data-labels>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-accessed-valid-bool.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-accessed-valid-bool.xml
new file mode 100644
index 0000000..f927bba
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-accessed-valid-bool.xml
@@ -0,0 +1,6 @@
+<data-labels>
+    <data-accessed dataCategory="location"
+        dataType="approx_location"
+        ephemeral="false"
+        purposes="app_functionality" />
+</data-labels>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-collected-invalid-bool.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-collected-invalid-bool.xml
new file mode 100644
index 0000000..ba11afb
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-collected-invalid-bool.xml
@@ -0,0 +1,7 @@
+<data-labels>
+    <data-collected dataCategory="location"
+        dataType="approx_location"
+        ephemeral="false"
+        isSharingOptional="false"
+        purposes="app_functionality" />
+</data-labels>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-collected-valid-bool.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-collected-valid-bool.xml
new file mode 100644
index 0000000..4b6d3977
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-collected-valid-bool.xml
@@ -0,0 +1,7 @@
+<data-labels>
+    <data-collected dataCategory="location"
+        dataType="approx_location"
+        ephemeral="false"
+        isCollectionOptional="false"
+        purposes="app_functionality" />
+</data-labels>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-shared-invalid-bool.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-shared-invalid-bool.xml
new file mode 100644
index 0000000..7840b98
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-shared-invalid-bool.xml
@@ -0,0 +1,7 @@
+<data-labels>
+    <data-shared dataCategory="location"
+        dataType="approx_location"
+        ephemeral="false"
+        isCollectionOptional="false"
+        purposes="app_functionality" />
+</data-labels>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-shared-valid-bool.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-shared-valid-bool.xml
new file mode 100644
index 0000000..ccf77b0
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-shared-valid-bool.xml
@@ -0,0 +1,7 @@
+<data-labels>
+    <data-shared dataCategory="location"
+        dataType="approx_location"
+        ephemeral="false"
+        isSharingOptional="false"
+        purposes="app_functionality" />
+</data-labels>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/od/data-labels-accessed-valid-bool.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/od/data-labels-accessed-valid-bool.xml
new file mode 100644
index 0000000..ddefc18
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/od/data-labels-accessed-valid-bool.xml
@@ -0,0 +1,12 @@
+<pbundle_as_map name="data_labels">
+    <pbundle_as_map name="data_accessed">
+        <pbundle_as_map name="location">
+            <pbundle_as_map name="approx_location">
+                <int-array name="purposes" num="1">
+                    <item value="1"/>
+                </int-array>
+                <boolean name="ephemeral" value="false"/>
+            </pbundle_as_map>
+        </pbundle_as_map>
+    </pbundle_as_map>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/od/data-labels-collected-valid-bool.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/od/data-labels-collected-valid-bool.xml
new file mode 100644
index 0000000..252c728
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/od/data-labels-collected-valid-bool.xml
@@ -0,0 +1,13 @@
+<pbundle_as_map name="data_labels">
+    <pbundle_as_map name="data_collected">
+        <pbundle_as_map name="location">
+            <pbundle_as_map name="approx_location">
+                <int-array name="purposes" num="1">
+                    <item value="1"/>
+                </int-array>
+                <boolean name="is_collection_optional" value="false"/>
+                <boolean name="ephemeral" value="false"/>
+            </pbundle_as_map>
+        </pbundle_as_map>
+    </pbundle_as_map>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/od/data-labels-shared-valid-bool.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/od/data-labels-shared-valid-bool.xml
new file mode 100644
index 0000000..d1d4e33
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/od/data-labels-shared-valid-bool.xml
@@ -0,0 +1,13 @@
+<pbundle_as_map name="data_labels">
+    <pbundle_as_map name="data_shared">
+        <pbundle_as_map name="location">
+            <pbundle_as_map name="approx_location">
+                <int-array name="purposes" num="1">
+                    <item value="1"/>
+                </int-array>
+                <boolean name="is_sharing_optional" value="false"/>
+                <boolean name="ephemeral" value="false"/>
+            </pbundle_as_map>
+        </pbundle_as_map>
+    </pbundle_as_map>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/developerinfo/hr/all-fields-valid.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/developerinfo/hr/all-fields-valid.xml
new file mode 100644
index 0000000..908d8ea2
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/developerinfo/hr/all-fields-valid.xml
@@ -0,0 +1,8 @@
+<developer-info
+    name="max"
+    email="max@example.com"
+    address="111 blah lane"
+    countryRegion="US"
+    relationship="aosp"
+    website="example.com"
+    registryId="registry_id" />
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/developerinfo/od/all-fields-valid.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/developerinfo/od/all-fields-valid.xml
new file mode 100644
index 0000000..784ec61
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/developerinfo/od/all-fields-valid.xml
@@ -0,0 +1,9 @@
+<pbundle_as_map name="developer_info">
+    <string name="name" value="max"/>
+    <string name="email" value="max@example.com"/>
+    <string name="address" value="111 blah lane"/>
+    <string name="country_region" value="US"/>
+    <long name="relationship" value="5"/>
+    <string name="website" value="example.com"/>
+    <string name="app_developer_registry_id" value="registry_id"/>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/hr/missing-version.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/hr/missing-version.xml
new file mode 100644
index 0000000..762f3bd
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/hr/missing-version.xml
@@ -0,0 +1,2 @@
+<safety-labels>
+</safety-labels>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/hr/valid-empty.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/hr/valid-empty.xml
new file mode 100644
index 0000000..7decfd4
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/hr/valid-empty.xml
@@ -0,0 +1 @@
+<safety-labels version="12345"></safety-labels>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/hr/with-data-labels.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/hr/with-data-labels.xml
new file mode 100644
index 0000000..8997f4f
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/hr/with-data-labels.xml
@@ -0,0 +1,9 @@
+<safety-labels version="12345">
+    <data-labels>
+        <data-shared dataCategory="location"
+            dataType="approx_location"
+            isSharingOptional="false"
+            ephemeral="false"
+            purposes="app_functionality" />
+    </data-labels>
+</safety-labels>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/od/valid-empty.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/od/valid-empty.xml
new file mode 100644
index 0000000..4f03d88
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/od/valid-empty.xml
@@ -0,0 +1,3 @@
+<pbundle_as_map name="safety_labels">
+    <long name="version" value="12345"/>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/od/with-data-labels.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/od/with-data-labels.xml
new file mode 100644
index 0000000..a966fda
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/od/with-data-labels.xml
@@ -0,0 +1,16 @@
+<pbundle_as_map name="safety_labels">
+    <long name="version" value="12345"/>
+    <pbundle_as_map name="data_labels">
+        <pbundle_as_map name="data_shared">
+            <pbundle_as_map name="location">
+                <pbundle_as_map name="approx_location">
+                    <int-array name="purposes" num="1">
+                        <item value="1"/>
+                    </int-array>
+                    <boolean name="is_sharing_optional" value="false"/>
+                    <boolean name="ephemeral" value="false"/>
+                </pbundle_as_map>
+            </pbundle_as_map>
+        </pbundle_as_map>
+    </pbundle_as_map>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/hr/missing-url.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/hr/missing-url.xml
new file mode 100644
index 0000000..ff26c05
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/hr/missing-url.xml
@@ -0,0 +1 @@
+<system-app-safety-label></system-app-safety-label>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/hr/valid.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/hr/valid.xml
new file mode 100644
index 0000000..6fe86c3
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/hr/valid.xml
@@ -0,0 +1 @@
+<system-app-safety-label url="www.example.com"></system-app-safety-label>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/od/valid.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/od/valid.xml
new file mode 100644
index 0000000..f96535b
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/od/valid.xml
@@ -0,0 +1,3 @@
+<pbundle_as_map name="system_app_safety_label">
+    <string name="url" value="www.example.com"/>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/valid-empty.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/valid-empty.xml
new file mode 100644
index 0000000..254a37f
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/valid-empty.xml
@@ -0,0 +1,4 @@
+
+<transparency-info>
+
+</transparency-info>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/with-app-info.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/with-app-info.xml
new file mode 100644
index 0000000..a7c48fc
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/with-app-info.xml
@@ -0,0 +1,4 @@
+
+<transparency-info>
+    <app-info title="beervision" description="a beer app" containsAds="true" obeyAps="false" adsFingerprinting="false" securityFingerprinting="false" privacyPolicy="www.example.com" securityEndpoints="url1|url2|url3" firstPartyEndpoints="url1" serviceProviderEndpoints="url55|url56" category="Food and drink" email="max@maxloh.com" />
+</transparency-info>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/with-developer-info.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/with-developer-info.xml
new file mode 100644
index 0000000..862bda4
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/with-developer-info.xml
@@ -0,0 +1,11 @@
+
+<transparency-info>
+    <developer-info
+        name="max"
+        email="max@example.com"
+        address="111 blah lane"
+        countryRegion="US"
+        relationship="aosp"
+        website="example.com"
+        appDeveloperRegistryId="registry_id" />
+</transparency-info>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-empty.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-empty.xml
new file mode 100644
index 0000000..af574cf
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-empty.xml
@@ -0,0 +1 @@
+<pbundle_as_map name="transparency_info"/>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-app-info.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-app-info.xml
new file mode 100644
index 0000000..b813641
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-app-info.xml
@@ -0,0 +1,26 @@
+
+<pbundle_as_map name="transparency_info">
+    <pbundle_as_map name="app_info">
+        <string name="title" value="beervision"/>
+        <string name="description" value="a beer app"/>
+        <boolean name="contains_ads" value="true"/>
+        <boolean name="obey_aps" value="false"/>
+        <boolean name="ads_fingerprinting" value="false"/>
+        <boolean name="security_fingerprinting" value="false"/>
+        <string name="privacy_policy" value="www.example.com"/>
+        <string-array name="security_endpoint" num="3">
+            <item value="url1"/>
+            <item value="url2"/>
+            <item value="url3"/>
+        </string-array>
+        <string-array name="first_party_endpoint" num="1">
+            <item value="url1"/>
+        </string-array>
+        <string-array name="service_provider_endpoint" num="2">
+            <item value="url55"/>
+            <item value="url56"/>
+        </string-array>
+        <string name="category" value="Food and drink"/>
+        <string name="email" value="max@maxloh.com"/>
+    </pbundle_as_map>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-developer-info.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-developer-info.xml
new file mode 100644
index 0000000..101c98b
--- /dev/null
+++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-developer-info.xml
@@ -0,0 +1,11 @@
+
+<pbundle_as_map name="transparency_info">
+    <pbundle_as_map name="developer_info">
+        <string name="name" value="max"/>
+        <string name="email" value="max@example.com"/>
+        <string name="address" value="111 blah lane"/>
+        <string name="country_region" value="US"/>
+        <long name="relationship" value="5"/>
+        <string name="website" value="example.com"/>
+    </pbundle_as_map>
+</pbundle_as_map>
\ No newline at end of file
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/aslgen/validmappings/contacts/hr.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/contacts/hr.xml
similarity index 100%
rename from tools/app_metadata_bundles/src/test/resources/com/android/aslgen/validmappings/contacts/hr.xml
rename to tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/contacts/hr.xml
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/aslgen/validmappings/contacts/od.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/contacts/od.xml
similarity index 100%
rename from tools/app_metadata_bundles/src/test/resources/com/android/aslgen/validmappings/contacts/od.xml
rename to tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/contacts/od.xml
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/aslgen/validmappings/location/hr.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/location/hr.xml
similarity index 100%
rename from tools/app_metadata_bundles/src/test/resources/com/android/aslgen/validmappings/location/hr.xml
rename to tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/location/hr.xml
diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/aslgen/validmappings/location/od.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/location/od.xml
similarity index 100%
rename from tools/app_metadata_bundles/src/test/resources/com/android/aslgen/validmappings/location/od.xml
rename to tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/location/od.xml