Merge "Remove usage of SK_MaxS32"
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobService.java b/apex/jobscheduler/framework/java/android/app/job/JobService.java
index 31d2266..1a205d0 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobService.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobService.java
@@ -73,16 +73,12 @@
      * Detach the notification supplied to
      * {@link #setNotification(JobParameters, int, Notification, int)} when the job ends.
      * The notification will remain shown even after JobScheduler stops the job.
-     *
-     * @hide
      */
     public static final int JOB_END_NOTIFICATION_POLICY_DETACH = 0;
     /**
      * Cancel and remove the notification supplied to
      * {@link #setNotification(JobParameters, int, Notification, int)} when the job ends.
      * The notification will be removed from the notification shade.
-     *
-     * @hide
      */
     public static final int JOB_END_NOTIFICATION_POLICY_REMOVE = 1;
 
diff --git a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java
index ed717c4..6998081 100644
--- a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java
+++ b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java
@@ -195,10 +195,35 @@
             return;
         }
 
+        if ("scheduling".equals(op)) {
+            setSchedulingEnabled(userId);
+            return;
+        }
+
         System.err.println("Unknown command");
         showUsage();
     }
 
+    private void setSchedulingEnabled(int userId) {
+        String arg = nextArg();
+        if (arg == null) {
+            showUsage();
+            return;
+        }
+
+        try {
+            boolean enable = Boolean.parseBoolean(arg);
+            mBmgr.setFrameworkSchedulingEnabledForUser(userId, enable);
+            System.out.println(
+                    "Backup scheduling is now "
+                            + (enable ? "enabled" : "disabled")
+                            + " for user "
+                            + userId);
+        } catch (RemoteException e) {
+            handleRemoteException(e);
+        }
+    }
+
     private void handleRemoteException(RemoteException e) {
         System.err.println(e.toString());
         System.err.println(BMGR_NOT_RUNNING_ERR);
@@ -944,6 +969,7 @@
         System.err.println("       bmgr activate BOOL");
         System.err.println("       bmgr activated");
         System.err.println("       bmgr autorestore BOOL");
+        System.err.println("       bmgr scheduling BOOL");
         System.err.println("");
         System.err.println("The '--user' option specifies the user on which the operation is run.");
         System.err.println("It must be the first argument before the operation.");
@@ -1021,6 +1047,9 @@
         System.err.println("");
         System.err.println("The 'autorestore' command enables or disables automatic restore when");
         System.err.println("a new package is installed.");
+        System.err.println("");
+        System.err.println("The 'scheduling' command enables or disables backup scheduling in the");
+        System.err.println("framework.");
     }
 
     private static class BackupMonitor extends IBackupManagerMonitor.Stub {
diff --git a/core/api/current.txt b/core/api/current.txt
index a6446ff..4ec261c 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -8043,24 +8043,24 @@
     field public static final int PACKAGE_POLICY_BLOCKLIST = 1; // 0x1
   }
 
-  public final class PolicyUpdateReason {
-    ctor public PolicyUpdateReason(int);
-    method public int getReasonCode();
-    field public static final int REASON_CONFLICTING_ADMIN_POLICY = 0; // 0x0
-    field public static final int REASON_UNKNOWN = -1; // 0xffffffff
+  public final class PolicyUpdateResult {
+    ctor public PolicyUpdateResult(int);
+    method public int getResultCode();
+    field public static final int RESULT_FAILURE_CONFLICTING_ADMIN_POLICY = 1; // 0x1
+    field public static final int RESULT_FAILURE_UNKNOWN = -1; // 0xffffffff
+    field public static final int RESULT_SUCCESS = 0; // 0x0
   }
 
   public abstract class PolicyUpdatesReceiver extends android.content.BroadcastReceiver {
     ctor public PolicyUpdatesReceiver();
-    method public void onPolicyChanged(@NonNull android.content.Context, @NonNull String, @NonNull android.os.Bundle, @NonNull android.app.admin.TargetUser, @NonNull android.app.admin.PolicyUpdateReason);
-    method public void onPolicySetResult(@NonNull android.content.Context, @NonNull String, @NonNull android.os.Bundle, @NonNull android.app.admin.TargetUser, int, @Nullable android.app.admin.PolicyUpdateReason);
+    method public void onPolicyChanged(@NonNull android.content.Context, @NonNull String, @NonNull android.os.Bundle, @NonNull android.app.admin.TargetUser, @NonNull android.app.admin.PolicyUpdateResult);
+    method public void onPolicySetResult(@NonNull android.content.Context, @NonNull String, @NonNull android.os.Bundle, @NonNull android.app.admin.TargetUser, @NonNull android.app.admin.PolicyUpdateResult);
     method public final void onReceive(android.content.Context, android.content.Intent);
     field public static final String ACTION_DEVICE_POLICY_CHANGED = "android.app.admin.action.DEVICE_POLICY_CHANGED";
     field public static final String ACTION_DEVICE_POLICY_SET_RESULT = "android.app.admin.action.DEVICE_POLICY_SET_RESULT";
+    field public static final String EXTRA_INTENT_FILTER = "android.app.admin.extra.INTENT_FILTER";
     field public static final String EXTRA_PACKAGE_NAME = "android.app.admin.extra.PACKAGE_NAME";
     field public static final String EXTRA_PERMISSION_NAME = "android.app.admin.extra.PERMISSION_NAME";
-    field public static final int POLICY_SET_RESULT_FAILURE = -1; // 0xffffffff
-    field public static final int POLICY_SET_RESULT_SUCCESS = 0; // 0x0
   }
 
   public final class PreferentialNetworkServiceConfig implements android.os.Parcelable {
@@ -8650,6 +8650,8 @@
     method public final void updateEstimatedNetworkBytes(@NonNull android.app.job.JobParameters, @NonNull android.app.job.JobWorkItem, long, long);
     method public final void updateTransferredNetworkBytes(@NonNull android.app.job.JobParameters, long, long);
     method public final void updateTransferredNetworkBytes(@NonNull android.app.job.JobParameters, @NonNull android.app.job.JobWorkItem, long, long);
+    field public static final int JOB_END_NOTIFICATION_POLICY_DETACH = 0; // 0x0
+    field public static final int JOB_END_NOTIFICATION_POLICY_REMOVE = 1; // 0x1
     field public static final String PERMISSION_BIND = "android.permission.BIND_JOB_SERVICE";
   }
 
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index ae032db..ce29937 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -311,6 +311,12 @@
     @PermissionMethod
     public abstract void enforceCallingPermission(@PermissionName String permission, String func);
 
+    /**
+     * Returns the current and target user ids as a {@link Pair}. Target user id will be
+     * {@link android.os.UserHandle#USER_NULL} if there is not an ongoing user switch.
+     */
+    public abstract Pair<Integer, Integer> getCurrentAndTargetUserIds();
+
     /** Returns the current user id. */
     public abstract int getCurrentUserId();
 
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 33721a0..11584cc 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -3964,6 +3964,9 @@
      * Called by a device owner, a profile owner of an organization-owned device or the system to
      * get the Memory Tagging Extension (MTE) policy
      *
+     * <a href="https://source.android.com/docs/security/test/memory-safety/arm-mte">
+     * Learn more about MTE</a>
+     *
      * @throws SecurityException if caller is not device owner or profile owner of org-owned device
      *                           or system uid, or if called on a parent instance
      * @return the currently set MTE policy
@@ -3986,6 +3989,7 @@
      */
     public static final String AUTO_TIMEZONE_POLICY = "autoTimezone";
 
+    // TODO: Expose this as SystemAPI once we add the query API
     /**
      * @hide
      */
@@ -4012,7 +4016,22 @@
     /**
      * @hide
      */
-    public static final String USER_CONTROL_DISABLED_PACKAGES = "userControlDisabledPackages";
+    public static final String USER_CONTROL_DISABLED_PACKAGES_POLICY =
+            "userControlDisabledPackages";
+
+
+    // TODO: Expose this as SystemAPI once we add the query API
+    /**
+     * @hide
+     */
+    public static final String PERSISTENT_PREFERRED_ACTIVITY_POLICY =
+            "persistentPreferredActivity";
+
+    // TODO: Expose this as SystemAPI once we add the query API
+    /**
+     * @hide
+     */
+    public static final String PACKAGE_UNINSTALL_BLOCKED_POLICY = "packageUninstallBlocked";
 
     /**
      * This object is a single place to tack on invalidation and disable calls.  All
diff --git a/core/java/android/app/admin/PolicyUpdateReason.java b/core/java/android/app/admin/PolicyUpdateReason.java
deleted file mode 100644
index 97d282d..0000000
--- a/core/java/android/app/admin/PolicyUpdateReason.java
+++ /dev/null
@@ -1,74 +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 android.app.admin;
-
-import android.annotation.IntDef;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * Class containing the reason a policy (set from {@link DevicePolicyManager}) hasn't been enforced
- * (passed in to {@link PolicyUpdatesReceiver#onPolicySetResult}) or has changed (passed in to
- * {@link PolicyUpdatesReceiver#onPolicyChanged}).
- */
-public final class PolicyUpdateReason {
-
-    /**
-     * Reason code to indicate that the policy has not been enforced or has changed for an unknown
-     * reason.
-     */
-    public static final int REASON_UNKNOWN = -1;
-
-    /**
-     * Reason code to indicate that the policy has not been enforced or has changed because another
-     * admin has set a conflicting policy on the device.
-     */
-    public static final int REASON_CONFLICTING_ADMIN_POLICY = 0;
-
-    /**
-     * Reason codes for {@link #getReasonCode()}.
-     *
-     * @hide
-     */
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef(flag = true, prefix = { "REASON_" }, value = {
-            REASON_UNKNOWN,
-            REASON_CONFLICTING_ADMIN_POLICY,
-    })
-    public @interface ReasonCode {}
-
-    private final int mReasonCode;
-
-    /**
-     * Constructor for {@code PolicyUpdateReason} that takes in a reason code describing why the
-     * policy has changed.
-     *
-     * @param reasonCode Describes why the policy has changed.
-     */
-    public PolicyUpdateReason(@ReasonCode int reasonCode) {
-        this.mReasonCode = reasonCode;
-    }
-
-    /**
-     * Returns reason code for why a policy hasn't been applied or has changed.
-     */
-    @ReasonCode
-    public int getReasonCode() {
-        return mReasonCode;
-    }
-}
diff --git a/core/java/android/app/admin/PolicyUpdateResult.java b/core/java/android/app/admin/PolicyUpdateResult.java
new file mode 100644
index 0000000..9e13e00
--- /dev/null
+++ b/core/java/android/app/admin/PolicyUpdateResult.java
@@ -0,0 +1,82 @@
+/*
+ * 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 android.app.admin;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Class containing the reason for the policy (set from {@link DevicePolicyManager}) update (e.g.
+ * success, failure reasons, etc.). This is passed in to
+ * {@link PolicyUpdatesReceiver#onPolicySetResult}) and
+ * {@link PolicyUpdatesReceiver#onPolicyChanged}).
+ */
+public final class PolicyUpdateResult {
+
+    /**
+     * Result code to indicate that the policy has not been enforced or has changed for an unknown
+     * reason.
+     */
+    public static final int RESULT_FAILURE_UNKNOWN = -1;
+
+    /**
+     * Result code to indicate that the policy has been changed to the desired value set by
+     * the admin.
+     */
+    public static final int RESULT_SUCCESS = 0;
+
+    /**
+     * Result code to indicate that the policy has not been enforced or has changed because another
+     * admin has set a conflicting policy on the device.
+     */
+    public static final int RESULT_FAILURE_CONFLICTING_ADMIN_POLICY = 1;
+
+    /**
+     * Reason codes for {@link #getResultCode()}.
+     *
+     * @hide
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(flag = true, prefix = { "RESULT_" }, value = {
+            RESULT_FAILURE_UNKNOWN,
+            RESULT_SUCCESS,
+            RESULT_FAILURE_CONFLICTING_ADMIN_POLICY
+    })
+    public @interface ResultCode {}
+
+    private final int mResultCode;
+
+    /**
+     * Constructor for {@code PolicyUpdateReason} that takes in a result code describing why the
+     * policy has changed.
+     *
+     * @param resultCode Describes why the policy has changed.
+     */
+    public PolicyUpdateResult(@ResultCode int resultCode) {
+        this.mResultCode = resultCode;
+    }
+
+    /**
+     * Returns result code describing why the policy has changed.
+     */
+    @ResultCode
+    public int getResultCode() {
+        return mResultCode;
+    }
+}
diff --git a/core/java/android/app/admin/PolicyUpdatesReceiver.java b/core/java/android/app/admin/PolicyUpdatesReceiver.java
index f7216e7..67de04c 100644
--- a/core/java/android/app/admin/PolicyUpdatesReceiver.java
+++ b/core/java/android/app/admin/PolicyUpdatesReceiver.java
@@ -17,9 +17,7 @@
 package android.app.admin;
 
 import android.annotation.BroadcastBehavior;
-import android.annotation.IntDef;
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.annotation.SdkConstant;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -27,8 +25,6 @@
 import android.os.Bundle;
 import android.util.Log;
 
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
 import java.util.Objects;
 
 /**
@@ -50,31 +46,6 @@
     private static String TAG = "PolicyUpdatesReceiver";
 
     /**
-     * Result code passed in to {@link #onPolicySetResult} to indicate that the policy has been
-     * set successfully.
-     */
-    public static final int POLICY_SET_RESULT_SUCCESS = 0;
-
-    /**
-     * Result code passed in to {@link #onPolicySetResult} to indicate that the policy has NOT been
-     * set, a {@link PolicyUpdateReason} will be passed in to {@link #onPolicySetResult} to indicate
-     * the reason.
-     */
-    public static final int POLICY_SET_RESULT_FAILURE = -1;
-
-    /**
-     * Result codes passed in to {@link #onPolicySetResult}.
-     *
-     * @hide
-     */
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef(flag = true, prefix = { "POLICY_SET_RESULT_" }, value = {
-            POLICY_SET_RESULT_SUCCESS,
-            POLICY_SET_RESULT_FAILURE,
-    })
-    public @interface ResultCode {}
-
-    /**
      * Action for a broadcast sent to admins to communicate back the result of setting a policy in
      * {@link DevicePolicyManager}.
      *
@@ -125,6 +96,14 @@
             "android.app.admin.extra.PERMISSION_NAME";
 
     /**
+     * An {@link android.content.IntentFilter} extra holding the intent filter the policy relates
+     * to, (see {@link PolicyUpdatesReceiver#onPolicyChanged} and
+     * {@link PolicyUpdatesReceiver#onPolicySetResult})
+     */
+    public static final String EXTRA_INTENT_FILTER =
+            "android.app.admin.extra.INTENT_FILTER";
+
+    /**
      * @hide
      */
     public static final String EXTRA_POLICY_CHANGED_KEY =
@@ -144,14 +123,8 @@
     /**
      * @hide
      */
-    public static final String EXTRA_POLICY_SET_RESULT_KEY =
-            "android.app.admin.extra.POLICY_SET_RESULT_KEY";
-
-    /**
-     * @hide
-     */
-    public static final String EXTRA_POLICY_UPDATE_REASON_KEY =
-            "android.app.admin.extra.POLICY_UPDATE_REASON_KEY";
+    public static final String EXTRA_POLICY_UPDATE_RESULT_KEY =
+            "android.app.admin.extra.POLICY_UPDATE_RESULT_KEY";
 
     /**
      * @hide
@@ -172,7 +145,7 @@
             case ACTION_DEVICE_POLICY_SET_RESULT:
                 Log.i(TAG, "Received ACTION_DEVICE_POLICY_SET_RESULT");
                 onPolicySetResult(context, getPolicyKey(intent), getPolicyExtraBundle(intent),
-                        getTargetUser(intent), getPolicyResult(intent), getFailureReason(intent));
+                        getTargetUser(intent), getPolicyChangedReason(intent));
                 break;
             case ACTION_DEVICE_POLICY_CHANGED:
                 Log.i(TAG, "Received ACTION_DEVICE_POLICY_CHANGED");
@@ -197,17 +170,6 @@
     /**
      * @hide
      */
-    @ResultCode
-    static int getPolicyResult(Intent intent) {
-        if (!intent.hasExtra(EXTRA_POLICY_SET_RESULT_KEY)) {
-            throw new IllegalArgumentException("Result has to be provided.");
-        }
-        return intent.getIntExtra(EXTRA_POLICY_SET_RESULT_KEY, POLICY_SET_RESULT_FAILURE);
-    }
-
-    /**
-     * @hide
-     */
     @NonNull
     static Bundle getPolicyExtraBundle(Intent intent) {
         Bundle bundle = intent.getBundleExtra(EXTRA_POLICY_BUNDLE_KEY);
@@ -217,22 +179,14 @@
     /**
      * @hide
      */
-    @Nullable
-    static PolicyUpdateReason getFailureReason(Intent intent) {
-        if (getPolicyResult(intent) != POLICY_SET_RESULT_FAILURE) {
-            return null;
-        }
-        return getPolicyChangedReason(intent);
-    }
-
-    /**
-     * @hide
-     */
     @NonNull
-    static PolicyUpdateReason getPolicyChangedReason(Intent intent) {
+    static PolicyUpdateResult getPolicyChangedReason(Intent intent) {
+        if (!intent.hasExtra(EXTRA_POLICY_UPDATE_RESULT_KEY)) {
+            throw new IllegalArgumentException("PolicyUpdateResult has to be provided.");
+        }
         int reasonCode = intent.getIntExtra(
-                EXTRA_POLICY_UPDATE_REASON_KEY, PolicyUpdateReason.REASON_UNKNOWN);
-        return new PolicyUpdateReason(reasonCode);
+                EXTRA_POLICY_UPDATE_RESULT_KEY, PolicyUpdateResult.RESULT_FAILURE_UNKNOWN);
+        return new PolicyUpdateResult(reasonCode);
     }
 
     /**
@@ -268,19 +222,18 @@
      *                               Each policy will document the required additional params if
      *                               needed.
      * @param targetUser The {@link TargetUser} which this policy relates to.
-     * @param result Indicates whether the policy has been set successfully,
-     *               (see {@link PolicyUpdatesReceiver#POLICY_SET_RESULT_SUCCESS} and
-     *               {@link PolicyUpdatesReceiver#POLICY_SET_RESULT_FAILURE}).
-     * @param reason Indicates the reason the policy failed to apply, {@code null} if the policy was
-     *               applied successfully.
+     * @param policyUpdateResult Indicates whether the policy has been set successfully
+     *                           ({@link PolicyUpdateResult#RESULT_SUCCESS}) or the reason it
+     *                           failed to apply (e.g.
+     *                           {@link PolicyUpdateResult#RESULT_FAILURE_CONFLICTING_ADMIN_POLICY},
+     *                           etc).
      */
     public void onPolicySetResult(
             @NonNull Context context,
             @NonNull String policyKey,
             @NonNull Bundle additionalPolicyParams,
             @NonNull TargetUser targetUser,
-            @ResultCode int result,
-            @Nullable PolicyUpdateReason reason) {}
+            @NonNull PolicyUpdateResult policyUpdateResult) {}
 
     // TODO(b/260847505): Add javadocs to explain which DPM APIs are supported
     // TODO(b/261430877): Add javadocs to explain when will this get triggered
@@ -302,12 +255,17 @@
      *                               Each policy will document the required additional params if
      *                               needed.
      * @param targetUser The {@link TargetUser} which this policy relates to.
-     * @param reason Indicates the reason the policy value has changed.
+     * @param policyUpdateResult Indicates the reason the policy value has changed
+     *                           (e.g. {@link PolicyUpdateResult#RESULT_SUCCESS} if the policy has
+     *                           changed to the value set by the admin,
+     *                           {@link PolicyUpdateResult#RESULT_FAILURE_CONFLICTING_ADMIN_POLICY}
+     *                           if the policy has changed because another admin has set a
+     *                           conflicting policy, etc)
      */
     public void onPolicyChanged(
             @NonNull Context context,
             @NonNull String policyKey,
             @NonNull Bundle additionalPolicyParams,
             @NonNull TargetUser targetUser,
-            @NonNull PolicyUpdateReason reason) {}
+            @NonNull PolicyUpdateResult policyUpdateResult) {}
 }
diff --git a/core/java/android/app/backup/BackupManager.java b/core/java/android/app/backup/BackupManager.java
index bad282e..ddeb2f9 100644
--- a/core/java/android/app/backup/BackupManager.java
+++ b/core/java/android/app/backup/BackupManager.java
@@ -16,7 +16,6 @@
 
 package android.app.backup;
 
-import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
@@ -38,8 +37,6 @@
 import android.util.Log;
 import android.util.Pair;
 
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
 import java.util.List;
 
 /**
@@ -410,6 +407,33 @@
     }
 
     /**
+     * Enable/disable the framework backup scheduling entirely for the current user. When disabled,
+     * no Key/Value or Full backup jobs will be scheduled by the Android framework.
+     *
+     * <p>Note: This does not disable backups: only their scheduling is affected and backups can
+     * still be triggered manually.
+     *
+     * <p>Callers must hold the android.permission.BACKUP permission to use this method.
+     *
+     * @hide
+     */
+    @RequiresPermission(allOf = {android.Manifest.permission.BACKUP,
+            android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}, conditional = true)
+    public void setFrameworkSchedulingEnabled(boolean isEnabled) {
+        checkServiceBinder();
+        if (sService == null) {
+            Log.e(TAG, "setFrameworkSchedulingEnabled() couldn't connect");
+            return;
+        }
+
+        try {
+            sService.setFrameworkSchedulingEnabledForUser(mContext.getUserId(), isEnabled);
+        } catch (RemoteException e) {
+            Log.e(TAG, "setFrameworkSchedulingEnabled() couldn't connect");
+        }
+    }
+
+    /**
      * Report whether the backup mechanism is currently enabled.
      *
      * @hide
diff --git a/core/java/android/app/backup/IBackupManager.aidl b/core/java/android/app/backup/IBackupManager.aidl
index aeb4987..041c2a7 100644
--- a/core/java/android/app/backup/IBackupManager.aidl
+++ b/core/java/android/app/backup/IBackupManager.aidl
@@ -155,6 +155,22 @@
      */
     void setBackupEnabledForUser(int userId, boolean isEnabled);
 
+
+    /**
+     * Enable/disable the framework backup scheduling entirely. When disabled, no Key/Value or Full
+     * backup jobs will be scheduled by the Android framework.
+     *
+     * <p>Note: This does not disable backups: only their scheduling is affected and backups can
+     * still be triggered manually.
+     *
+     * <p>Callers must hold the android.permission.BACKUP permission to use this method. If
+     * {@code userId} is different from the calling user id, then the caller must additionally hold
+     * the android.permission.INTERACT_ACROSS_USERS_FULL permission.
+     *
+     * @param userId The user for which backup scheduling should be enabled/disabled.
+     */
+    void setFrameworkSchedulingEnabledForUser(int userId, boolean isEnabled);
+
     /**
      * {@link android.app.backup.IBackupManager.setBackupEnabledForUser} for the calling user id.
      */
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl
index 0e37c87..047f8c1 100644
--- a/core/java/android/content/pm/IPackageManager.aidl
+++ b/core/java/android/content/pm/IPackageManager.aidl
@@ -276,6 +276,8 @@
 
     void clearPackagePersistentPreferredActivities(String packageName, int userId);
 
+    void clearPersistentPreferredActivity(in IntentFilter filter, int userId);
+
     void addCrossProfileIntentFilter(in IntentFilter intentFilter, String ownerPackage,
             int sourceUserId, int targetUserId, int flags);
 
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 9a848af..b510fad 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -8660,6 +8660,12 @@
         public static final String BACKUP_AUTO_RESTORE = "backup_auto_restore";
 
         /**
+         * Controls whether framework backup scheduling is enabled.
+         * @hide
+         */
+        public static final String BACKUP_SCHEDULING_ENABLED = "backup_scheduling_enabled";
+
+        /**
          * Indicates whether settings backup has been fully provisioned.
          * Type: int ( 0 = unprovisioned, 1 = fully provisioned )
          * @hide
diff --git a/core/java/com/android/internal/app/OWNERS b/core/java/com/android/internal/app/OWNERS
index 970728f..56da8e0 100644
--- a/core/java/com/android/internal/app/OWNERS
+++ b/core/java/com/android/internal/app/OWNERS
@@ -3,6 +3,7 @@
 per-file *Chooser* = file:/packages/SystemUI/OWNERS
 per-file SimpleIconFactory.java = file:/packages/SystemUI/OWNERS
 per-file AbstractMultiProfilePagerAdapter.java = file:/packages/SystemUI/OWNERS
+per-file *EmptyStateProvider.java = file:/packages/SystemUI/OWNERS
 per-file NetInitiatedActivity.java = file:/location/java/android/location/OWNERS
 per-file *BatteryStats* = file:/BATTERY_STATS_OWNERS
 
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index 8fb345b..a704eb3 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -337,4 +337,11 @@
      * @param leftOrTop indicates where the stage split is.
      */
     void enterStageSplitFromRunningApp(boolean leftOrTop);
+
+    /**
+     * Shows the media output switcher dialog.
+     *
+     * @param packageName of the session for which the output switcher is shown.
+     */
+    void showMediaOutputSwitcher(String packageName);
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java b/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java
index 44a37f4..d4d2b48 100644
--- a/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.content.res.Resources;
+import android.hardware.display.DisplayManager;
 import android.os.AsyncTask;
 import android.os.RemoteException;
 import android.os.UserHandle;
@@ -32,6 +33,9 @@
 import com.android.settingslib.R;
 
 import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Predicate;
 
 /**
  * Utility methods for working with display density.
@@ -70,120 +74,169 @@
      */
     private static final int MIN_DIMENSION_DP = 320;
 
-    private final String[] mEntries;
-    private final int[] mValues;
+    private static final Predicate<DisplayInfo> INTERNAL_ONLY =
+            (info) -> info.type == Display.TYPE_INTERNAL;
 
-    private final int mDefaultDensity;
-    private final int mCurrentIndex;
+    private final Predicate<DisplayInfo> mPredicate;
+
+    private final DisplayManager mDisplayManager;
+
+    /**
+     * The text description of the density values of the default display.
+     */
+    private String[] mDefaultDisplayDensityEntries;
+
+    /**
+     * The density values of the default display.
+     */
+    private int[] mDefaultDisplayDensityValues;
+
+    /**
+     * The density values, indexed by display unique ID.
+     */
+    private final Map<String, int[]> mValuesPerDisplay = new HashMap();
+
+    private int mDefaultDensityForDefaultDisplay;
+    private int mCurrentIndex = -1;
 
     public DisplayDensityUtils(Context context) {
-        final int defaultDensity = DisplayDensityUtils.getDefaultDisplayDensity(
-                Display.DEFAULT_DISPLAY);
-        if (defaultDensity <= 0) {
-            mEntries = null;
-            mValues = null;
-            mDefaultDensity = 0;
-            mCurrentIndex = -1;
-            return;
-        }
+        this(context, INTERNAL_ONLY);
+    }
 
-        final Resources res = context.getResources();
-        DisplayInfo info = new DisplayInfo();
-        context.getDisplayNoVerify().getDisplayInfo(info);
+    /**
+     * Creates an instance that stores the density values for the displays that satisfy
+     * the predicate.
+     * @param context The context
+     * @param predicate Determines what displays the density should be set for. The default display
+     *                  must satisfy this predicate.
+     */
+    public DisplayDensityUtils(Context context, Predicate predicate) {
+        mPredicate = predicate;
+        mDisplayManager = context.getSystemService(DisplayManager.class);
 
-        final int currentDensity = info.logicalDensityDpi;
-        int currentDensityIndex = -1;
-
-        // Compute number of "larger" and "smaller" scales for this display.
-        final int minDimensionPx = Math.min(info.logicalWidth, info.logicalHeight);
-        final int maxDensity = DisplayMetrics.DENSITY_MEDIUM * minDimensionPx / MIN_DIMENSION_DP;
-        final float maxScaleDimen = context.getResources().getFraction(
-                R.fraction.display_density_max_scale, 1, 1);
-        final float maxScale = Math.min(maxScaleDimen, maxDensity / (float) defaultDensity);
-        final float minScale = context.getResources().getFraction(
-                R.fraction.display_density_min_scale, 1, 1);
-        final float minScaleInterval = context.getResources().getFraction(
-                R.fraction.display_density_min_scale_interval, 1, 1);
-        final int numLarger = (int) MathUtils.constrain((maxScale - 1) / minScaleInterval,
-                0, SUMMARIES_LARGER.length);
-        final int numSmaller = (int) MathUtils.constrain((1 - minScale) / minScaleInterval,
-                0, SUMMARIES_SMALLER.length);
-
-        String[] entries = new String[1 + numSmaller + numLarger];
-        int[] values = new int[entries.length];
-        int curIndex = 0;
-
-        if (numSmaller > 0) {
-            final float interval = (1 - minScale) / numSmaller;
-            for (int i = numSmaller - 1; i >= 0; i--) {
-                // Round down to a multiple of 2 by truncating the low bit.
-                final int density = ((int) (defaultDensity * (1 - (i + 1) * interval))) & ~1;
-                if (currentDensity == density) {
-                    currentDensityIndex = curIndex;
-                }
-                entries[curIndex] = res.getString(SUMMARIES_SMALLER[i]);
-                values[curIndex] = density;
-                curIndex++;
+        for (Display display : mDisplayManager.getDisplays(
+                DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED)) {
+            DisplayInfo info = new DisplayInfo();
+            if (!display.getDisplayInfo(info)) {
+                Log.w(LOG_TAG, "Cannot fetch display info for display " + display.getDisplayId());
+                continue;
             }
-        }
-
-        if (currentDensity == defaultDensity) {
-            currentDensityIndex = curIndex;
-        }
-        values[curIndex] = defaultDensity;
-        entries[curIndex] = res.getString(SUMMARY_DEFAULT);
-        curIndex++;
-
-        if (numLarger > 0) {
-            final float interval = (maxScale - 1) / numLarger;
-            for (int i = 0; i < numLarger; i++) {
-                // Round down to a multiple of 2 by truncating the low bit.
-                final int density = ((int) (defaultDensity * (1 + (i + 1) * interval))) & ~1;
-                if (currentDensity == density) {
-                    currentDensityIndex = curIndex;
+            if (!mPredicate.test(info)) {
+                if (display.getDisplayId() == Display.DEFAULT_DISPLAY) {
+                    throw new IllegalArgumentException("Predicate must not filter out the default "
+                            + "display.");
                 }
-                values[curIndex] = density;
-                entries[curIndex] = res.getString(SUMMARIES_LARGER[i]);
-                curIndex++;
+                continue;
             }
+
+            final int defaultDensity = DisplayDensityUtils.getDefaultDensityForDisplay(
+                    display.getDisplayId());
+            if (defaultDensity <= 0) {
+                Log.w(LOG_TAG, "Cannot fetch default density for display "
+                        + display.getDisplayId());
+                continue;
+            }
+
+            final Resources res = context.getResources();
+
+            final int currentDensity = info.logicalDensityDpi;
+            int currentDensityIndex = -1;
+
+            // Compute number of "larger" and "smaller" scales for this display.
+            final int minDimensionPx = Math.min(info.logicalWidth, info.logicalHeight);
+            final int maxDensity =
+                    DisplayMetrics.DENSITY_MEDIUM * minDimensionPx / MIN_DIMENSION_DP;
+            final float maxScaleDimen = context.getResources().getFraction(
+                    R.fraction.display_density_max_scale, 1, 1);
+            final float maxScale = Math.min(maxScaleDimen, maxDensity / (float) defaultDensity);
+            final float minScale = context.getResources().getFraction(
+                    R.fraction.display_density_min_scale, 1, 1);
+            final float minScaleInterval = context.getResources().getFraction(
+                    R.fraction.display_density_min_scale_interval, 1, 1);
+            final int numLarger = (int) MathUtils.constrain((maxScale - 1) / minScaleInterval,
+                    0, SUMMARIES_LARGER.length);
+            final int numSmaller = (int) MathUtils.constrain((1 - minScale) / minScaleInterval,
+                    0, SUMMARIES_SMALLER.length);
+
+            String[] entries = new String[1 + numSmaller + numLarger];
+            int[] values = new int[entries.length];
+            int curIndex = 0;
+
+            if (numSmaller > 0) {
+                final float interval = (1 - minScale) / numSmaller;
+                for (int i = numSmaller - 1; i >= 0; i--) {
+                    // Round down to a multiple of 2 by truncating the low bit.
+                    final int density = ((int) (defaultDensity * (1 - (i + 1) * interval))) & ~1;
+                    if (currentDensity == density) {
+                        currentDensityIndex = curIndex;
+                    }
+                    entries[curIndex] = res.getString(SUMMARIES_SMALLER[i]);
+                    values[curIndex] = density;
+                    curIndex++;
+                }
+            }
+
+            if (currentDensity == defaultDensity) {
+                currentDensityIndex = curIndex;
+            }
+            values[curIndex] = defaultDensity;
+            entries[curIndex] = res.getString(SUMMARY_DEFAULT);
+            curIndex++;
+
+            if (numLarger > 0) {
+                final float interval = (maxScale - 1) / numLarger;
+                for (int i = 0; i < numLarger; i++) {
+                    // Round down to a multiple of 2 by truncating the low bit.
+                    final int density = ((int) (defaultDensity * (1 + (i + 1) * interval))) & ~1;
+                    if (currentDensity == density) {
+                        currentDensityIndex = curIndex;
+                    }
+                    values[curIndex] = density;
+                    entries[curIndex] = res.getString(SUMMARIES_LARGER[i]);
+                    curIndex++;
+                }
+            }
+
+            final int displayIndex;
+            if (currentDensityIndex >= 0) {
+                displayIndex = currentDensityIndex;
+            } else {
+                // We don't understand the current density. Must have been set by
+                // someone else. Make room for another entry...
+                int newLength = values.length + 1;
+                values = Arrays.copyOf(values, newLength);
+                values[curIndex] = currentDensity;
+
+                entries = Arrays.copyOf(entries, newLength);
+                entries[curIndex] = res.getString(SUMMARY_CUSTOM, currentDensity);
+
+                displayIndex = curIndex;
+            }
+
+            if (display.getDisplayId() == Display.DEFAULT_DISPLAY) {
+                mDefaultDensityForDefaultDisplay = defaultDensity;
+                mCurrentIndex = displayIndex;
+                mDefaultDisplayDensityEntries = entries;
+                mDefaultDisplayDensityValues = values;
+            }
+            mValuesPerDisplay.put(info.uniqueId, values);
         }
-
-        final int displayIndex;
-        if (currentDensityIndex >= 0) {
-            displayIndex = currentDensityIndex;
-        } else {
-            // We don't understand the current density. Must have been set by
-            // someone else. Make room for another entry...
-            int newLength = values.length + 1;
-            values = Arrays.copyOf(values, newLength);
-            values[curIndex] = currentDensity;
-
-            entries = Arrays.copyOf(entries, newLength);
-            entries[curIndex] = res.getString(SUMMARY_CUSTOM, currentDensity);
-
-            displayIndex = curIndex;
-        }
-
-        mDefaultDensity = defaultDensity;
-        mCurrentIndex = displayIndex;
-        mEntries = entries;
-        mValues = values;
     }
 
-    public String[] getEntries() {
-        return mEntries;
+    public String[] getDefaultDisplayDensityEntries() {
+        return mDefaultDisplayDensityEntries;
     }
 
-    public int[] getValues() {
-        return mValues;
+    public int[] getDefaultDisplayDensityValues() {
+        return mDefaultDisplayDensityValues;
     }
 
-    public int getCurrentIndex() {
+    public int getCurrentIndexForDefaultDisplay() {
         return mCurrentIndex;
     }
 
-    public int getDefaultDensity() {
-        return mDefaultDensity;
+    public int getDefaultDensityForDefaultDisplay() {
+        return mDefaultDensityForDefaultDisplay;
     }
 
     /**
@@ -193,7 +246,7 @@
      * @return the default density of the specified display, or {@code -1} if
      *         the display does not exist or the density could not be obtained
      */
-    private static int getDefaultDisplayDensity(int displayId) {
+    private static int getDefaultDensityForDisplay(int displayId) {
        try {
            final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
            return wm.getInitialDisplayDensity(displayId);
@@ -203,19 +256,31 @@
     }
 
     /**
-     * Asynchronously applies display density changes to the specified display.
+     * Asynchronously applies display density changes to the displays that satisfy the predicate.
      * <p>
      * The change will be applied to the user specified by the value of
      * {@link UserHandle#myUserId()} at the time the method is called.
-     *
-     * @param displayId the identifier of the display to modify
      */
-    public static void clearForcedDisplayDensity(final int displayId) {
+    public void clearForcedDisplayDensity() {
         final int userId = UserHandle.myUserId();
         AsyncTask.execute(() -> {
             try {
-                final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
-                wm.clearForcedDisplayDensityForUser(displayId, userId);
+                for (Display display : mDisplayManager.getDisplays(
+                        DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED)) {
+                    int displayId = display.getDisplayId();
+                    DisplayInfo info = new DisplayInfo();
+                    if (!display.getDisplayInfo(info)) {
+                        Log.w(LOG_TAG, "Unable to clear forced display density setting "
+                                + "for display " + displayId);
+                        continue;
+                    }
+                    if (!mPredicate.test(info)) {
+                        continue;
+                    }
+
+                    final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
+                    wm.clearForcedDisplayDensityForUser(displayId, userId);
+                }
             } catch (RemoteException exc) {
                 Log.w(LOG_TAG, "Unable to clear forced display density setting");
             }
@@ -223,20 +288,39 @@
     }
 
     /**
-     * Asynchronously applies display density changes to the specified display.
+     * Asynchronously applies display density changes to the displays that satisfy the predicate.
      * <p>
      * The change will be applied to the user specified by the value of
      * {@link UserHandle#myUserId()} at the time the method is called.
      *
-     * @param displayId the identifier of the display to modify
-     * @param density the density to force for the specified display
+     * @param index The index of the density value
      */
-    public static void setForcedDisplayDensity(final int displayId, final int density) {
+    public void setForcedDisplayDensity(final int index) {
         final int userId = UserHandle.myUserId();
         AsyncTask.execute(() -> {
             try {
-                final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
-                wm.setForcedDisplayDensityForUser(displayId, density, userId);
+                for (Display display : mDisplayManager.getDisplays(
+                        DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED)) {
+                    int displayId = display.getDisplayId();
+                    DisplayInfo info = new DisplayInfo();
+                    if (!display.getDisplayInfo(info)) {
+                        Log.w(LOG_TAG, "Unable to save forced display density setting "
+                                + "for display " + displayId);
+                        continue;
+                    }
+                    if (!mPredicate.test(info)) {
+                        continue;
+                    }
+                    if (!mValuesPerDisplay.containsKey(info.uniqueId)) {
+                        Log.w(LOG_TAG, "Unable to save forced display density setting "
+                                + "for display " + info.uniqueId);
+                        continue;
+                    }
+
+                    final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
+                    wm.setForcedDisplayDensityForUser(displayId,
+                            mValuesPerDisplay.get(info.uniqueId)[index], userId);
+                }
             } catch (RemoteException exc) {
                 Log.w(LOG_TAG, "Unable to save forced display density setting");
             }
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 4365a9b..5ee36f3 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -695,6 +695,7 @@
                  Settings.Secure.BACKUP_AUTO_RESTORE,
                  Settings.Secure.BACKUP_ENABLED,
                  Settings.Secure.BACKUP_PROVISIONED,
+                 Settings.Secure.BACKUP_SCHEDULING_ENABLED,
                  Settings.Secure.BACKUP_TRANSPORT,
                  Settings.Secure.CALL_SCREENING_DEFAULT_COMPONENT,
                  Settings.Secure.CAMERA_LIFT_TRIGGER_ENABLED, // Candidate for backup?
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 59f68f7..9cbc64e 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.keyboard.KeyboardUI
 import com.android.systemui.keyguard.KeyguardViewMediator
 import com.android.systemui.log.SessionTracker
+import com.android.systemui.media.dialog.MediaOutputSwitcherDialogUI
 import com.android.systemui.media.RingtonePlayer
 import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper
 import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver
@@ -218,6 +219,12 @@
     @ClassKey(ToastUI::class)
     abstract fun bindToastUI(service: ToastUI): CoreStartable
 
+    /** Inject into MediaOutputSwitcherDialogUI.  */
+    @Binds
+    @IntoMap
+    @ClassKey(MediaOutputSwitcherDialogUI::class)
+    abstract fun MediaOutputSwitcherDialogUI(sysui: MediaOutputSwitcherDialogUI): CoreStartable
+
     /** Inject into VolumeUI.  */
     @Binds
     @IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
index c73387b..72c7cf5 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
@@ -119,6 +119,7 @@
     private boolean mListening;
     private boolean mListeningTouchScreenSensors;
     private boolean mListeningProxSensors;
+    private boolean mListeningAodOnlySensors;
     private boolean mUdfpsEnrolled;
 
     @DevicePostureController.DevicePostureInt
@@ -187,7 +188,8 @@
                         dozeParameters.getPulseOnSigMotion(),
                         DozeLog.PULSE_REASON_SENSOR_SIGMOTION,
                         false /* touchCoords */,
-                        false /* touchscreen */),
+                        false /* touchscreen */
+                ),
                 new TriggerSensor(
                         mSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE),
                         Settings.Secure.DOZE_PICK_UP_GESTURE,
@@ -198,14 +200,17 @@
                         false /* touchscreen */,
                         false /* ignoresSetting */,
                         false /* requires prox */,
-                        true /* immediatelyReRegister */),
+                        true /* immediatelyReRegister */,
+                        false /* requiresAod */
+                ),
                 new TriggerSensor(
                         findSensor(config.doubleTapSensorType()),
                         Settings.Secure.DOZE_DOUBLE_TAP_GESTURE,
                         true /* configured */,
                         DozeLog.REASON_SENSOR_DOUBLE_TAP,
                         dozeParameters.doubleTapReportsTouchCoordinates(),
-                        true /* touchscreen */),
+                        true /* touchscreen */
+                ),
                 new TriggerSensor(
                         findSensors(config.tapSensorTypeMapping()),
                         Settings.Secure.DOZE_TAP_SCREEN_GESTURE,
@@ -217,7 +222,9 @@
                         false /* ignoresSetting */,
                         dozeParameters.singleTapUsesProx(mDevicePosture) /* requiresProx */,
                         true /* immediatelyReRegister */,
-                        mDevicePosture),
+                        mDevicePosture,
+                        false
+                ),
                 new TriggerSensor(
                         findSensor(config.longPressSensorType()),
                         Settings.Secure.DOZE_PULSE_ON_LONG_PRESS,
@@ -228,7 +235,9 @@
                         true /* touchscreen */,
                         false /* ignoresSetting */,
                         dozeParameters.longPressUsesProx() /* requiresProx */,
-                        true /* immediatelyReRegister */),
+                        true /* immediatelyReRegister */,
+                        false /* requiresAod */
+                ),
                 new TriggerSensor(
                         findSensor(config.udfpsLongPressSensorType()),
                         "doze_pulse_on_auth",
@@ -239,7 +248,9 @@
                         true /* touchscreen */,
                         false /* ignoresSetting */,
                         dozeParameters.longPressUsesProx(),
-                        false /* immediatelyReRegister */),
+                        false /* immediatelyReRegister */,
+                        true /* requiresAod */
+                ),
                 new PluginSensor(
                         new SensorManagerPlugin.Sensor(TYPE_WAKE_DISPLAY),
                         Settings.Secure.DOZE_WAKE_DISPLAY_GESTURE,
@@ -247,7 +258,8 @@
                           && mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT),
                         DozeLog.REASON_SENSOR_WAKE_UP_PRESENCE,
                         false /* reports touch coordinates */,
-                        false /* touchscreen */),
+                        false /* touchscreen */
+                ),
                 new PluginSensor(
                         new SensorManagerPlugin.Sensor(TYPE_WAKE_LOCK_SCREEN),
                         Settings.Secure.DOZE_WAKE_LOCK_SCREEN_GESTURE,
@@ -255,7 +267,8 @@
                         DozeLog.PULSE_REASON_SENSOR_WAKE_REACH,
                         false /* reports touch coordinates */,
                         false /* touchscreen */,
-                        mConfig.getWakeLockScreenDebounce()),
+                        mConfig.getWakeLockScreenDebounce()
+                ),
                 new TriggerSensor(
                         findSensor(config.quickPickupSensorType()),
                         Settings.Secure.DOZE_QUICK_PICKUP_GESTURE,
@@ -266,7 +279,9 @@
                         false /* requiresTouchscreen */,
                         false /* ignoresSetting */,
                         false /* requiresProx */,
-                        true /* immediatelyReRegister */),
+                        true /* immediatelyReRegister */,
+                        false /* requiresAod */
+                ),
         };
         setProxListening(false);  // Don't immediately start listening when we register.
         mProximitySensor.register(
@@ -360,29 +375,36 @@
     /**
      * If sensors should be registered and sending signals.
      */
-    public void setListening(boolean listen, boolean includeTouchScreenSensors) {
-        if (mListening == listen && mListeningTouchScreenSensors == includeTouchScreenSensors) {
+    public void setListening(boolean listen, boolean includeTouchScreenSensors,
+            boolean includeAodOnlySensors) {
+        if (mListening == listen && mListeningTouchScreenSensors == includeTouchScreenSensors
+                && mListeningAodOnlySensors == includeAodOnlySensors) {
             return;
         }
         mListening = listen;
         mListeningTouchScreenSensors = includeTouchScreenSensors;
+        mListeningAodOnlySensors = includeAodOnlySensors;
         updateListening();
     }
 
     /**
      * If sensors should be registered and sending signals.
      */
-    public void setListening(boolean listen, boolean includeTouchScreenSensors,
-            boolean lowPowerStateOrOff) {
+    public void setListeningWithPowerState(boolean listen, boolean includeTouchScreenSensors,
+            boolean includeAodRequiringSensors, boolean lowPowerStateOrOff) {
         final boolean shouldRegisterProxSensors =
                 !mSelectivelyRegisterProxSensors || lowPowerStateOrOff;
-        if (mListening == listen && mListeningTouchScreenSensors == includeTouchScreenSensors
-                && mListeningProxSensors == shouldRegisterProxSensors) {
+        if (mListening == listen
+                && mListeningTouchScreenSensors == includeTouchScreenSensors
+                && mListeningProxSensors == shouldRegisterProxSensors
+                && mListeningAodOnlySensors == includeAodRequiringSensors
+        ) {
             return;
         }
         mListening = listen;
         mListeningTouchScreenSensors = includeTouchScreenSensors;
         mListeningProxSensors = shouldRegisterProxSensors;
+        mListeningAodOnlySensors = includeAodRequiringSensors;
         updateListening();
     }
 
@@ -394,7 +416,8 @@
         for (TriggerSensor s : mTriggerSensors) {
             boolean listen = mListening
                     && (!s.mRequiresTouchscreen || mListeningTouchScreenSensors)
-                    && (!s.mRequiresProx || mListeningProxSensors);
+                    && (!s.mRequiresProx || mListeningProxSensors)
+                    && (!s.mRequiresAod || mListeningAodOnlySensors);
             s.setListening(listen);
             if (listen) {
                 anyListening = true;
@@ -502,6 +525,9 @@
         private final boolean mRequiresTouchscreen;
         private final boolean mRequiresProx;
 
+        // Whether the sensor should only register if the device is in AOD
+        private final boolean mRequiresAod;
+
         // Whether to immediately re-register this sensor after the sensor is triggered.
         // If false, the sensor registration will be updated on the next AOD state transition.
         private final boolean mImmediatelyReRegister;
@@ -530,7 +556,8 @@
                     requiresTouchscreen,
                     false /* ignoresSetting */,
                     false /* requiresProx */,
-                    true /* immediatelyReRegister */
+                    true /* immediatelyReRegister */,
+                    false
             );
         }
 
@@ -544,7 +571,8 @@
                 boolean requiresTouchscreen,
                 boolean ignoresSetting,
                 boolean requiresProx,
-                boolean immediatelyReRegister
+                boolean immediatelyReRegister,
+                boolean requiresAod
         ) {
             this(
                     new Sensor[]{ sensor },
@@ -557,7 +585,8 @@
                     ignoresSetting,
                     requiresProx,
                     immediatelyReRegister,
-                    DevicePostureController.DEVICE_POSTURE_UNKNOWN
+                    DevicePostureController.DEVICE_POSTURE_UNKNOWN,
+                    requiresAod
             );
         }
 
@@ -572,7 +601,8 @@
                 boolean ignoresSetting,
                 boolean requiresProx,
                 boolean immediatelyReRegister,
-                @DevicePostureController.DevicePostureInt int posture
+                @DevicePostureController.DevicePostureInt int posture,
+                boolean requiresAod
         ) {
             mSensors = sensors;
             mSetting = setting;
@@ -583,6 +613,7 @@
             mRequiresTouchscreen = requiresTouchscreen;
             mIgnoresSetting = ignoresSetting;
             mRequiresProx = requiresProx;
+            mRequiresAod = requiresAod;
             mPosture = posture;
             mImmediatelyReRegister = immediatelyReRegister;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
index b95c3f3..b709608 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
@@ -111,6 +111,7 @@
     private boolean mWantProxSensor;
     private boolean mWantTouchScreenSensors;
     private boolean mWantSensors;
+    private boolean mInAod;
 
     private final UserTracker.Callback mUserChangedCallback =
             new UserTracker.Callback() {
@@ -460,12 +461,19 @@
                 mDozeSensors.requestTemporaryDisable();
                 break;
             case DOZE:
-            case DOZE_AOD:
                 mAodInterruptRunnable = null;
-                mWantProxSensor = newState != DozeMachine.State.DOZE;
+                mWantProxSensor = false;
                 mWantSensors = true;
                 mWantTouchScreenSensors = true;
-                if (newState == DozeMachine.State.DOZE_AOD && !sWakeDisplaySensorState) {
+                mInAod = false;
+                break;
+            case DOZE_AOD:
+                mAodInterruptRunnable = null;
+                mWantProxSensor = true;
+                mWantSensors = true;
+                mWantTouchScreenSensors = true;
+                mInAod = true;
+                if (!sWakeDisplaySensorState) {
                     onWakeScreen(false, newState, DozeLog.REASON_SENSOR_WAKE_UP_PRESENCE);
                 }
                 break;
@@ -491,7 +499,7 @@
                 break;
             default:
         }
-        mDozeSensors.setListening(mWantSensors, mWantTouchScreenSensors);
+        mDozeSensors.setListening(mWantSensors, mWantTouchScreenSensors, mInAod);
     }
 
     private void registerCallbacks() {
@@ -510,11 +518,12 @@
 
     private void stopListeningToAllTriggers() {
         unregisterCallbacks();
-        mDozeSensors.setListening(false, false);
+        mDozeSensors.setListening(false, false, false);
         mDozeSensors.setProxListening(false);
         mWantSensors = false;
         mWantProxSensor = false;
         mWantTouchScreenSensors = false;
+        mInAod = false;
     }
 
     @Override
@@ -523,7 +532,8 @@
         final boolean lowPowerStateOrOff = state == Display.STATE_DOZE
                 || state == Display.STATE_DOZE_SUSPEND || state == Display.STATE_OFF;
         mDozeSensors.setProxListening(mWantProxSensor && lowPowerStateOrOff);
-        mDozeSensors.setListening(mWantSensors, mWantTouchScreenSensors, lowPowerStateOrOff);
+        mDozeSensors.setListeningWithPowerState(mWantSensors, mWantTouchScreenSensors,
+                mInAod, lowPowerStateOrOff);
 
         if (mAodInterruptRunnable != null && state == Display.STATE_ON) {
             mAodInterruptRunnable.run();
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index a5d9fee..22052d9 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -531,6 +531,11 @@
     @JvmField
     val OUTPUT_SWITCHER_DEVICE_STATUS = unreleasedFlag(2502, "output_switcher_device_status")
 
+    // TODO(b/20911786): Tracking Bug
+    @JvmField
+    val OUTPUT_SWITCHER_SHOW_API_ENABLED =
+        unreleasedFlag(2503, "output_switcher_show_api_enabled", teamfood = true)
+
     // TODO(b259590361): Tracking bug
     val EXPERIMENTAL_FLAG = unreleasedFlag(2, "exp_flag_release")
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java
new file mode 100644
index 0000000..e35575b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java
@@ -0,0 +1,71 @@
+/*
+ * 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.media.dialog;
+
+import android.annotation.MainThread;
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.systemui.CoreStartable;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.statusbar.CommandQueue;
+
+import javax.inject.Inject;
+
+/** Controls display of media output switcher. */
+@SysUISingleton
+public class MediaOutputSwitcherDialogUI implements CoreStartable, CommandQueue.Callbacks {
+
+    private static final String TAG = "MediaOutputSwitcherDialogUI";
+
+    private final CommandQueue mCommandQueue;
+    private final MediaOutputDialogFactory mMediaOutputDialogFactory;
+    private final FeatureFlags mFeatureFlags;
+
+    @Inject
+    public MediaOutputSwitcherDialogUI(
+            Context context,
+            CommandQueue commandQueue,
+            MediaOutputDialogFactory mediaOutputDialogFactory,
+            FeatureFlags featureFlags) {
+        mCommandQueue = commandQueue;
+        mMediaOutputDialogFactory = mediaOutputDialogFactory;
+        mFeatureFlags = featureFlags;
+    }
+
+    @Override
+    public void start() {
+        if (mFeatureFlags.isEnabled(Flags.OUTPUT_SWITCHER_SHOW_API_ENABLED)) {
+            mCommandQueue.addCallback(this);
+        } else {
+            Log.w(TAG, "Show media output switcher is not enabled.");
+        }
+    }
+
+    @Override
+    @MainThread
+    public void showMediaOutputSwitcher(String packageName) {
+        if (!TextUtils.isEmpty(packageName)) {
+            mMediaOutputDialogFactory.create(packageName, false, null);
+        } else {
+            Log.e(TAG, "Unable to launch media output dialog. Package name is empty.");
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index 245a55d..3e6eb05 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -525,6 +525,15 @@
     }
 
     private void updateIsEnabled() {
+        try {
+            Trace.beginSection("EdgeBackGestureHandler#updateIsEnabled");
+            updateIsEnabledTraced();
+        } finally {
+            Trace.endSection();
+        }
+    }
+
+    private void updateIsEnabledTraced() {
         boolean isEnabled = mIsAttached && mIsGesturalModeEnabled;
         if (isEnabled == mIsEnabled) {
             return;
@@ -611,14 +620,16 @@
     }
 
     private void setEdgeBackPlugin(NavigationEdgeBackPlugin edgeBackPlugin) {
-        if (mEdgeBackPlugin != null) {
-            mEdgeBackPlugin.onDestroy();
+        try {
+            Trace.beginSection("setEdgeBackPlugin");
+            mEdgeBackPlugin = edgeBackPlugin;
+            mEdgeBackPlugin.setBackCallback(mBackCallback);
+            mEdgeBackPlugin.setMotionEventsHandler(mMotionEventsHandler);
+            mEdgeBackPlugin.setLayoutParams(createLayoutParams());
+            updateDisplaySize();
+        } finally {
+            Trace.endSection();
         }
-        mEdgeBackPlugin = edgeBackPlugin;
-        mEdgeBackPlugin.setBackCallback(mBackCallback);
-        mEdgeBackPlugin.setMotionEventsHandler(mMotionEventsHandler);
-        mEdgeBackPlugin.setLayoutParams(createLayoutParams());
-        updateDisplaySize();
     }
 
     public boolean isHandlingGestures() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 04adaae..6e4ed7b0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -43,12 +43,14 @@
 import android.inputmethodservice.InputMethodService.BackDispositionMode;
 import android.media.INearbyMediaDevicesProvider;
 import android.media.MediaRoute2Info;
+import android.os.Binder;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
 import android.os.ParcelFileDescriptor;
+import android.os.Process;
 import android.os.RemoteException;
 import android.util.Pair;
 import android.util.SparseArray;
@@ -166,6 +168,7 @@
     private static final int MSG_SHOW_REAR_DISPLAY_DIALOG = 69 << MSG_SHIFT;
     private static final int MSG_GO_TO_FULLSCREEN_FROM_SPLIT = 70 << MSG_SHIFT;
     private static final int MSG_ENTER_STAGE_SPLIT_FROM_RUNNING_APP = 71 << MSG_SHIFT;
+    private static final int MSG_SHOW_MEDIA_OUTPUT_SWITCHER = 72 << MSG_SHIFT;
 
     public static final int FLAG_EXCLUDE_NONE = 0;
     public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0;
@@ -490,6 +493,11 @@
          * @see IStatusBar#enterStageSplitFromRunningApp
          */
         default void enterStageSplitFromRunningApp(boolean leftOrTop) {}
+
+        /**
+         * @see IStatusBar#showMediaOutputSwitcher
+         */
+        default void showMediaOutputSwitcher(String packageName) {}
     }
 
     public CommandQueue(Context context) {
@@ -1259,6 +1267,19 @@
     }
 
     @Override
+    public void showMediaOutputSwitcher(String packageName) {
+        int callingUid = Binder.getCallingUid();
+        if (callingUid != 0 && callingUid != Process.SYSTEM_UID) {
+            throw new SecurityException("Call only allowed from system server.");
+        }
+        synchronized (mLock) {
+            SomeArgs args = SomeArgs.obtain();
+            args.arg1 = packageName;
+            mHandler.obtainMessage(MSG_SHOW_MEDIA_OUTPUT_SWITCHER, args).sendToTarget();
+        }
+    }
+
+    @Override
     public void requestAddTile(
             @NonNull ComponentName componentName,
             @NonNull CharSequence appName,
@@ -1774,6 +1795,13 @@
                         mCallbacks.get(i).enterStageSplitFromRunningApp((Boolean) msg.obj);
                     }
                     break;
+                case MSG_SHOW_MEDIA_OUTPUT_SWITCHER:
+                    args = (SomeArgs) msg.obj;
+                    String clientPackageName = (String) args.arg1;
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).showMediaOutputSwitcher(clientPackageName);
+                    }
+                    break;
             }
         }
     }
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 4b56594..ec08bd4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -295,6 +295,7 @@
 
     private final Context mContext;
     private final LockscreenShadeTransitionController mLockscreenShadeTransitionController;
+    private final DeviceStateManager mDeviceStateManager;
     private CentralSurfacesCommandQueueCallbacks mCommandQueueCallbacks;
     private float mTransitionToFullShadeProgress = 0f;
     private NotificationListContainer mNotifListContainer;
@@ -862,8 +863,7 @@
         mMessageRouter.subscribeTo(MSG_LAUNCH_TRANSITION_TIMEOUT,
                 id -> onLaunchTransitionTimeout());
 
-        deviceStateManager.registerCallback(mMainExecutor,
-                new FoldStateListener(mContext, this::onFoldedStateChanged));
+        mDeviceStateManager = deviceStateManager;
         wiredChargingRippleController.registerCallbacks();
 
         mLightRevealScrimViewModelLazy = lightRevealScrimViewModelLazy;
@@ -1052,6 +1052,8 @@
             }
         });
 
+        registerCallbacks();
+
         mFalsingManager.addFalsingBeliefListener(mFalsingBeliefListener);
 
         mPluginManager.addPluginListener(
@@ -1107,6 +1109,14 @@
     }
 
     @VisibleForTesting
+    /** Registers listeners/callbacks with external dependencies. */
+    void registerCallbacks() {
+        //TODO(b/264502026) move the rest of the listeners here.
+        mDeviceStateManager.registerCallback(mMainExecutor,
+                new FoldStateListener(mContext, this::onFoldedStateChanged));
+    }
+
+    @VisibleForTesting
     void initShadeVisibilityListener() {
         mShadeController.setVisibilityListener(new ShadeController.ShadeVisibilityListener() {
             @Override
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt
index 7726d09..8214822 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldHapticsPlayer.kt
@@ -3,26 +3,43 @@
 import android.os.SystemProperties
 import android.os.VibrationEffect
 import android.os.Vibrator
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.systemui.unfold.updates.FoldProvider
+import com.android.systemui.unfold.updates.FoldProvider.FoldCallback
+import java.util.concurrent.Executor
 import javax.inject.Inject
 
-/**
- * Class that plays a haptics effect during unfolding a foldable device
- */
+/** Class that plays a haptics effect during unfolding a foldable device */
 @SysUIUnfoldScope
 class UnfoldHapticsPlayer
 @Inject
 constructor(
     unfoldTransitionProgressProvider: UnfoldTransitionProgressProvider,
+    foldProvider: FoldProvider,
+    @Main private val mainExecutor: Executor,
     private val vibrator: Vibrator?
 ) : TransitionProgressListener {
 
+    private var isFirstAnimationAfterUnfold = false
+
     init {
         if (vibrator != null) {
             // We don't need to remove the callback because we should listen to it
             // the whole time when SystemUI process is alive
             unfoldTransitionProgressProvider.addCallback(this)
         }
+
+        foldProvider.registerCallback(
+            object : FoldCallback {
+                override fun onFoldUpdated(isFolded: Boolean) {
+                    if (isFolded) {
+                        isFirstAnimationAfterUnfold = true
+                    }
+                }
+            },
+            mainExecutor
+        )
     }
 
     private var lastTransitionProgress = TRANSITION_PROGRESS_FULL_OPEN
@@ -36,6 +53,13 @@
     }
 
     override fun onTransitionFinishing() {
+        // Run haptics only when unfolding the device (first animation after unfolding)
+        if (!isFirstAnimationAfterUnfold) {
+            return
+        }
+
+        isFirstAnimationAfterUnfold = false
+
         // Run haptics only if the animation is long enough to notice
         if (lastTransitionProgress < TRANSITION_NOTICEABLE_THRESHOLD) {
             playHaptics()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
index cefba62..b6da649 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
@@ -146,7 +146,7 @@
 
     @Test
     public void testSensorDebounce() {
-        mDozeSensors.setListening(true, true);
+        mDozeSensors.setListening(true, true, true);
 
         mWakeLockScreenListener.onSensorChanged(mock(SensorManagerPlugin.SensorEvent.class));
         mTestableLooper.processAllMessages();
@@ -164,7 +164,7 @@
     @Test
     public void testSetListening_firstTrue_registerSettingsObserver() {
         verify(mSensorManager, never()).registerListener(any(), any(Sensor.class), anyInt());
-        mDozeSensors.setListening(true, true);
+        mDozeSensors.setListening(true, true, true);
 
         verify(mTriggerSensor).registerSettingsObserver(any(ContentObserver.class));
     }
@@ -172,8 +172,8 @@
     @Test
     public void testSetListening_twiceTrue_onlyRegisterSettingsObserverOnce() {
         verify(mSensorManager, never()).registerListener(any(), any(Sensor.class), anyInt());
-        mDozeSensors.setListening(true, true);
-        mDozeSensors.setListening(true, true);
+        mDozeSensors.setListening(true, true, true);
+        mDozeSensors.setListening(true, true, true);
 
         verify(mTriggerSensor, times(1)).registerSettingsObserver(any(ContentObserver.class));
     }
@@ -198,7 +198,7 @@
         assertFalse(mSensorTap.mRequested);
 
         // WHEN we're now in a low powered state
-        dozeSensors.setListening(true, true, true);
+        dozeSensors.setListeningWithPowerState(true, true, true, true);
 
         // THEN the tap sensor is registered
         assertTrue(mSensorTap.mRequested);
@@ -209,12 +209,12 @@
         // GIVEN doze sensors enabled
         when(mAmbientDisplayConfiguration.enabled(anyInt())).thenReturn(true);
 
-        // GIVEN a trigger sensor
+        // GIVEN a trigger sensor that's enabled by settings
         Sensor mockSensor = mock(Sensor.class);
-        TriggerSensor triggerSensor = mDozeSensors.createDozeSensor(
+        TriggerSensor triggerSensor = mDozeSensors.createDozeSensorWithSettingEnabled(
                 mockSensor,
-                /* settingEnabled */ true,
-                /* requiresTouchScreen */ true);
+                /* settingEnabled */ true
+        );
         when(mSensorManager.requestTriggerSensor(eq(triggerSensor), eq(mockSensor)))
                 .thenReturn(true);
 
@@ -230,12 +230,12 @@
         // GIVEN doze sensors enabled
         when(mAmbientDisplayConfiguration.enabled(anyInt())).thenReturn(true);
 
-        // GIVEN a trigger sensor
+        // GIVEN a trigger sensor that's not enabled by settings
         Sensor mockSensor = mock(Sensor.class);
-        TriggerSensor triggerSensor = mDozeSensors.createDozeSensor(
+        TriggerSensor triggerSensor = mDozeSensors.createDozeSensorWithSettingEnabled(
                 mockSensor,
-                /* settingEnabled*/ false,
-                /* requiresTouchScreen */ true);
+                /* settingEnabled*/ false
+        );
         when(mSensorManager.requestTriggerSensor(eq(triggerSensor), eq(mockSensor)))
                 .thenReturn(true);
 
@@ -251,12 +251,12 @@
         // GIVEN doze sensors enabled
         when(mAmbientDisplayConfiguration.enabled(anyInt())).thenReturn(true);
 
-        // GIVEN a trigger sensor that's
+        // GIVEN a trigger sensor that's not enabled by settings
         Sensor mockSensor = mock(Sensor.class);
-        TriggerSensor triggerSensor = mDozeSensors.createDozeSensor(
+        TriggerSensor triggerSensor = mDozeSensors.createDozeSensorWithSettingEnabled(
                 mockSensor,
-                /* settingEnabled*/ false,
-                /* requiresTouchScreen */ true);
+                /* settingEnabled*/ false
+        );
         when(mSensorManager.requestTriggerSensor(eq(triggerSensor), eq(mockSensor)))
                 .thenReturn(true);
 
@@ -266,7 +266,7 @@
         // WHEN ignoreSetting is called
         triggerSensor.ignoreSetting(true);
 
-        // THEN the sensor is registered
+        // THEN the sensor is still registered since the setting is ignore
         assertTrue(triggerSensor.mRegistered);
     }
 
@@ -277,10 +277,10 @@
 
         // GIVEN a trigger sensor
         Sensor mockSensor = mock(Sensor.class);
-        TriggerSensor triggerSensor = mDozeSensors.createDozeSensor(
+        TriggerSensor triggerSensor = mDozeSensors.createDozeSensorWithSettingEnabled(
                 mockSensor,
-                /* settingEnabled*/ true,
-                /* requiresTouchScreen */ true);
+                /* settingEnabled*/ true
+        );
         when(mSensorManager.requestTriggerSensor(eq(triggerSensor), eq(mockSensor)))
                 .thenReturn(true);
 
@@ -297,7 +297,7 @@
         // GIVEN doze sensor that supports postures
         Sensor closedSensor = createSensor(Sensor.TYPE_LIGHT, Sensor.STRING_TYPE_LIGHT);
         Sensor openedSensor = createSensor(Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_LIGHT);
-        TriggerSensor triggerSensor = mDozeSensors.createDozeSensor(
+        TriggerSensor triggerSensor = mDozeSensors.createDozeSensorForPosture(
                 new Sensor[] {
                         null /* unknown */,
                         closedSensor,
@@ -318,7 +318,7 @@
         // GIVEN doze sensor that supports postures
         Sensor closedSensor = createSensor(Sensor.TYPE_LIGHT, Sensor.STRING_TYPE_LIGHT);
         Sensor openedSensor = createSensor(Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_LIGHT);
-        TriggerSensor triggerSensor = mDozeSensors.createDozeSensor(
+        TriggerSensor triggerSensor = mDozeSensors.createDozeSensorForPosture(
                 new Sensor[] {
                         null /* unknown */,
                         closedSensor,
@@ -347,7 +347,7 @@
         // GIVEN doze sensor that supports postures
         Sensor closedSensor = createSensor(Sensor.TYPE_LIGHT, Sensor.STRING_TYPE_LIGHT);
         Sensor openedSensor = createSensor(Sensor.TYPE_PROXIMITY, Sensor.STRING_TYPE_LIGHT);
-        TriggerSensor triggerSensor = mDozeSensors.createDozeSensor(
+        TriggerSensor triggerSensor = mDozeSensors.createDozeSensorForPosture(
                 new Sensor[] {
                         null /* unknown */,
                         closedSensor,
@@ -402,7 +402,7 @@
     public void testUdfpsEnrollmentChanged() throws Exception {
         // GIVEN a UDFPS_LONG_PRESS trigger sensor that's not configured
         Sensor mockSensor = mock(Sensor.class);
-        TriggerSensor triggerSensor = mDozeSensors.createDozeSensor(
+        TriggerSensor triggerSensor = mDozeSensors.createDozeSensorForPosture(
                 mockSensor,
                 REASON_SENSOR_UDFPS_LONG_PRESS,
                 /* configured */ false);
@@ -411,7 +411,7 @@
                 .thenReturn(true);
 
         // WHEN listening state is set to TRUE
-        mDozeSensors.setListening(true, true);
+        mDozeSensors.setListening(true, true, true);
 
         // THEN mRegistered is still false b/c !mConfigured
         assertFalse(triggerSensor.mConfigured);
@@ -441,6 +441,35 @@
     }
 
     @Test
+    public void aodOnlySensor_onlyRegisteredWhenAodSensorsIncluded() {
+        // GIVEN doze sensors enabled
+        when(mAmbientDisplayConfiguration.enabled(anyInt())).thenReturn(true);
+
+        // GIVEN a trigger sensor that requires aod
+        Sensor mockSensor = mock(Sensor.class);
+        TriggerSensor aodOnlyTriggerSensor = mDozeSensors.createDozeSensorRequiringAod(mockSensor);
+        when(mSensorManager.requestTriggerSensor(eq(aodOnlyTriggerSensor), eq(mockSensor)))
+                .thenReturn(true);
+        mDozeSensors.addSensor(aodOnlyTriggerSensor);
+
+        // WHEN aod only sensors aren't included
+        mDozeSensors.setListening(/* listen */ true, /* includeTouchScreenSensors */true,
+                /* includeAodOnlySensors */false);
+
+        // THEN the sensor is not registered or requested
+        assertFalse(aodOnlyTriggerSensor.mRequested);
+        assertFalse(aodOnlyTriggerSensor.mRegistered);
+
+        // WHEN aod only sensors ARE included
+        mDozeSensors.setListening(/* listen */ true, /* includeTouchScreenSensors */true,
+                /* includeAodOnlySensors */true);
+
+        // THEN the sensor is registered and requested
+        assertTrue(aodOnlyTriggerSensor.mRequested);
+        assertTrue(aodOnlyTriggerSensor.mRegistered);
+    }
+
+    @Test
     public void liftToWake_defaultSetting_configDefaultFalse() {
         // WHEN the default lift to wake gesture setting is false
         when(mResources.getBoolean(
@@ -496,8 +525,8 @@
             mTriggerSensors = new TriggerSensor[] {mTriggerSensor, mSensorTap};
         }
 
-        public TriggerSensor createDozeSensor(Sensor sensor, boolean settingEnabled,
-                boolean requiresTouchScreen) {
+        public TriggerSensor createDozeSensorWithSettingEnabled(Sensor sensor,
+                boolean settingEnabled) {
             return new TriggerSensor(/* sensor */ sensor,
                     /* setting name */ "test_setting",
                     /* settingDefault */ settingEnabled,
@@ -506,11 +535,13 @@
                     /* reportsTouchCoordinate*/ false,
                     /* requiresTouchscreen */ false,
                     /* ignoresSetting */ false,
-                    requiresTouchScreen,
-                    /* immediatelyReRegister */ true);
+                    /* requiresProx */ false,
+                    /* immediatelyReRegister */ true,
+                    /* requiresAod */false
+            );
         }
 
-        public TriggerSensor createDozeSensor(
+        public TriggerSensor createDozeSensorForPosture(
                 Sensor sensor,
                 int pulseReason,
                 boolean configured
@@ -524,15 +555,35 @@
                     /* requiresTouchscreen */ false,
                     /* ignoresSetting */ false,
                     /* requiresTouchScreen */ false,
-                    /* immediatelyReRegister*/ true);
+                    /* immediatelyReRegister*/ true,
+                    false
+            );
         }
 
         /**
-         * create a doze sensor that supports postures and is enabled
+         * Create a doze sensor that requires Aod
          */
-        public TriggerSensor createDozeSensor(Sensor[] sensors, int posture) {
+        public TriggerSensor createDozeSensorRequiringAod(Sensor sensor) {
+            return new TriggerSensor(/* sensor */ sensor,
+                    /* setting name */ "aod_requiring_sensor",
+                    /* settingDefault */ true,
+                    /* configured */ true,
+                    /* pulseReason*/ 0,
+                    /* reportsTouchCoordinate*/ false,
+                    /* requiresTouchscreen */ false,
+                    /* ignoresSetting */ false,
+                    /* requiresProx */ false,
+                    /* immediatelyReRegister */ true,
+                    /* requiresAoD */ true
+            );
+        }
+
+        /**
+         * Create a doze sensor that supports postures and is enabled
+         */
+        public TriggerSensor createDozeSensorForPosture(Sensor[] sensors, int posture) {
             return new TriggerSensor(/* sensor */ sensors,
-                    /* setting name */ "test_setting",
+                    /* setting name */ "posture_test_setting",
                     /* settingDefault */ true,
                     /* configured */ true,
                     /* pulseReason*/ 0,
@@ -541,7 +592,9 @@
                     /* ignoresSetting */ true,
                     /* requiresProx */ false,
                     /* immediatelyReRegister */ true,
-                    posture);
+                    posture,
+                    /* requiresUi */ false
+            );
         }
 
         public void addSensor(TriggerSensor sensor) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
index b66a454..3552399 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
@@ -395,6 +395,14 @@
         verify(mAuthController).onAodInterrupt(anyInt(), anyInt(), anyFloat(), anyFloat());
     }
 
+
+    @Test
+    public void udfpsLongPress_dozeState_notRegistered() {
+        // GIVEN device is DOZE_AOD_PAUSED
+        when(mMachine.getState()).thenReturn(DozeMachine.State.DOZE);
+        // beverlyt
+    }
+
     private void waitForSensorManager() {
         mExecutor.runAllReady();
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index c8157cc..4c1b219 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -544,6 +544,7 @@
         mCentralSurfaces.startKeyguard();
         mInitController.executePostInitTasks();
         notificationLogger.setUpWithContainer(mNotificationListContainer);
+        mCentralSurfaces.registerCallbacks();
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt
index c316402..4a28cd1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/TestUnfoldTransitionProvider.kt
@@ -26,6 +26,10 @@
         listeners.forEach { it.onTransitionFinished() }
     }
 
+    override fun onTransitionFinishing() {
+        listeners.forEach { it.onTransitionFinishing() }
+    }
+
     override fun onTransitionProgress(progress: Float) {
         listeners.forEach { it.onTransitionProgress(progress) }
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt
new file mode 100644
index 0000000..d3fdbd9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldHapticsPlayerTest.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.unfold
+
+import android.os.VibrationEffect
+import android.os.Vibrator
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.unfold.updates.FoldProvider
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import java.util.concurrent.Executor
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class UnfoldHapticsPlayerTest : SysuiTestCase() {
+
+    private val progressProvider = TestUnfoldTransitionProvider()
+    private val vibrator: Vibrator = mock()
+    private val testFoldProvider = TestFoldProvider()
+
+    private lateinit var player: UnfoldHapticsPlayer
+
+    @Before
+    fun before() {
+        player = UnfoldHapticsPlayer(progressProvider, testFoldProvider, Runnable::run, vibrator)
+    }
+
+    @Test
+    fun testUnfoldingTransitionFinishingEarly_playsHaptics() {
+        testFoldProvider.onFoldUpdate(isFolded = true)
+        testFoldProvider.onFoldUpdate(isFolded = false)
+        progressProvider.onTransitionStarted()
+        progressProvider.onTransitionProgress(0.5f)
+        progressProvider.onTransitionFinishing()
+
+        verify(vibrator).vibrate(any<VibrationEffect>())
+    }
+
+    @Test
+    fun testUnfoldingTransitionFinishingLate_doesNotPlayHaptics() {
+        testFoldProvider.onFoldUpdate(isFolded = true)
+        testFoldProvider.onFoldUpdate(isFolded = false)
+        progressProvider.onTransitionStarted()
+        progressProvider.onTransitionProgress(0.99f)
+        progressProvider.onTransitionFinishing()
+
+        verify(vibrator, never()).vibrate(any<VibrationEffect>())
+    }
+
+    @Test
+    fun testFoldingAfterUnfolding_doesNotPlayHaptics() {
+        // Unfold
+        testFoldProvider.onFoldUpdate(isFolded = true)
+        testFoldProvider.onFoldUpdate(isFolded = false)
+        progressProvider.onTransitionStarted()
+        progressProvider.onTransitionProgress(0.5f)
+        progressProvider.onTransitionFinishing()
+        progressProvider.onTransitionFinished()
+        clearInvocations(vibrator)
+
+        // Fold
+        progressProvider.onTransitionStarted()
+        progressProvider.onTransitionProgress(0.5f)
+        progressProvider.onTransitionFinished()
+        testFoldProvider.onFoldUpdate(isFolded = true)
+
+        verify(vibrator, never()).vibrate(any<VibrationEffect>())
+    }
+
+    @Test
+    fun testUnfoldingAfterFoldingAndUnfolding_playsHaptics() {
+        // Unfold
+        testFoldProvider.onFoldUpdate(isFolded = true)
+        testFoldProvider.onFoldUpdate(isFolded = false)
+        progressProvider.onTransitionStarted()
+        progressProvider.onTransitionProgress(0.5f)
+        progressProvider.onTransitionFinishing()
+        progressProvider.onTransitionFinished()
+
+        // Fold
+        progressProvider.onTransitionStarted()
+        progressProvider.onTransitionProgress(0.5f)
+        progressProvider.onTransitionFinished()
+        testFoldProvider.onFoldUpdate(isFolded = true)
+        clearInvocations(vibrator)
+
+        // Unfold again
+        testFoldProvider.onFoldUpdate(isFolded = true)
+        testFoldProvider.onFoldUpdate(isFolded = false)
+        progressProvider.onTransitionStarted()
+        progressProvider.onTransitionProgress(0.5f)
+        progressProvider.onTransitionFinishing()
+        progressProvider.onTransitionFinished()
+
+        verify(vibrator).vibrate(any<VibrationEffect>())
+    }
+
+    private class TestFoldProvider : FoldProvider {
+        private val listeners = arrayListOf<FoldProvider.FoldCallback>()
+
+        override fun registerCallback(callback: FoldProvider.FoldCallback, executor: Executor) {
+            listeners += callback
+        }
+
+        override fun unregisterCallback(callback: FoldProvider.FoldCallback) {
+            listeners -= callback
+        }
+
+        fun onFoldUpdate(isFolded: Boolean) {
+            listeners.forEach { it.onFoldUpdated(isFolded) }
+        }
+    }
+}
diff --git a/services/backup/java/com/android/server/backup/BackupManagerService.java b/services/backup/java/com/android/server/backup/BackupManagerService.java
index 8c09dcd..dc475f6 100644
--- a/services/backup/java/com/android/server/backup/BackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/BackupManagerService.java
@@ -25,7 +25,6 @@
 import android.app.ActivityManager;
 import android.app.admin.DevicePolicyManager;
 import android.app.backup.BackupManager;
-import android.app.backup.BackupRestoreEventLogger;
 import android.app.backup.BackupRestoreEventLogger.DataTypeResult;
 import android.app.backup.IBackupManager;
 import android.app.backup.IBackupManagerMonitor;
@@ -723,6 +722,17 @@
     }
 
     @Override
+    public void setFrameworkSchedulingEnabledForUser(int userId, boolean isEnabled) {
+        UserBackupManagerService userBackupManagerService =
+                getServiceForUserIfCallerHasPermission(userId,
+                        "setFrameworkSchedulingEnabledForUser()");
+
+        if (userBackupManagerService != null) {
+            userBackupManagerService.setFrameworkSchedulingEnabled(isEnabled);
+        }
+    }
+
+    @Override
     public void setBackupEnabledForUser(@UserIdInt int userId, boolean isEnabled)
             throws RemoteException {
         if (isUserReadyForBackup(userId)) {
diff --git a/services/backup/java/com/android/server/backup/FullBackupJob.java b/services/backup/java/com/android/server/backup/FullBackupJob.java
index 0bb25e3..fe0e1c6 100644
--- a/services/backup/java/com/android/server/backup/FullBackupJob.java
+++ b/services/backup/java/com/android/server/backup/FullBackupJob.java
@@ -45,9 +45,12 @@
     private final SparseArray<JobParameters> mParamsForUser = new SparseArray<>();
 
     public static void schedule(int userId, Context ctx, long minDelay,
-            BackupManagerConstants constants) {
+            UserBackupManagerService userBackupManagerService) {
+        if (!userBackupManagerService.isFrameworkSchedulingEnabled()) return;
+
         JobScheduler js = (JobScheduler) ctx.getSystemService(Context.JOB_SCHEDULER_SERVICE);
         JobInfo.Builder builder = new JobInfo.Builder(getJobIdForUserId(userId), sIdleService);
+        final BackupManagerConstants constants = userBackupManagerService.getConstants();
         synchronized (constants) {
             builder.setRequiresDeviceIdle(true)
                     .setRequiredNetworkType(constants.getFullBackupRequiredNetworkType())
diff --git a/services/backup/java/com/android/server/backup/KeyValueBackupJob.java b/services/backup/java/com/android/server/backup/KeyValueBackupJob.java
index 058dcae..164bbea 100644
--- a/services/backup/java/com/android/server/backup/KeyValueBackupJob.java
+++ b/services/backup/java/com/android/server/backup/KeyValueBackupJob.java
@@ -64,14 +64,16 @@
     @VisibleForTesting
     public static final int MAX_JOB_ID = 52418896;
 
-    public static void schedule(int userId, Context ctx, BackupManagerConstants constants) {
-        schedule(userId, ctx, 0, constants);
+    public static void schedule(int userId, Context ctx,
+            UserBackupManagerService userBackupManagerService) {
+        schedule(userId, ctx, 0, userBackupManagerService);
     }
 
     public static void schedule(int userId, Context ctx, long delay,
-            BackupManagerConstants constants) {
+            UserBackupManagerService userBackupManagerService) {
         synchronized (KeyValueBackupJob.class) {
-            if (sScheduledForUserId.get(userId)) {
+            if (sScheduledForUserId.get(userId)
+                    || !userBackupManagerService.isFrameworkSchedulingEnabled()) {
                 return;
             }
 
@@ -80,6 +82,7 @@
             final int networkType;
             final boolean needsCharging;
 
+            final BackupManagerConstants constants = userBackupManagerService.getConstants();
             synchronized (constants) {
                 interval = constants.getKeyValueBackupIntervalMilliseconds();
                 fuzz = constants.getKeyValueBackupFuzzMilliseconds();
diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
index 6ba01d7..2c8bfeb 100644
--- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
@@ -1958,8 +1958,10 @@
             }
             // We don't want the backup jobs to kick in any time soon.
             // Reschedules them to run in the distant future.
-            KeyValueBackupJob.schedule(mUserId, mContext, BUSY_BACKOFF_MIN_MILLIS, mConstants);
-            FullBackupJob.schedule(mUserId, mContext, 2 * BUSY_BACKOFF_MIN_MILLIS, mConstants);
+            KeyValueBackupJob.schedule(mUserId, mContext, BUSY_BACKOFF_MIN_MILLIS,
+                    /* userBackupManagerService */ this);
+            FullBackupJob.schedule(mUserId, mContext, 2 * BUSY_BACKOFF_MIN_MILLIS,
+                    /* userBackupManagerService */ this);
         } finally {
             Binder.restoreCallingIdentity(oldToken);
         }
@@ -2088,7 +2090,8 @@
                 final long interval = mConstants.getFullBackupIntervalMilliseconds();
                 final long appLatency = (timeSinceLast < interval) ? (interval - timeSinceLast) : 0;
                 final long latency = Math.max(transportMinLatency, appLatency);
-                FullBackupJob.schedule(mUserId, mContext, latency, mConstants);
+                FullBackupJob.schedule(mUserId, mContext, latency,
+                        /* userBackupManagerService */ this);
             } else {
                 if (DEBUG_SCHEDULING) {
                     Slog.i(
@@ -2226,7 +2229,8 @@
                         addUserIdToLogMessage(
                                 mUserId, "Deferring scheduled full backups in battery saver mode"));
             }
-            FullBackupJob.schedule(mUserId, mContext, keyValueBackupInterval, mConstants);
+            FullBackupJob.schedule(mUserId, mContext, keyValueBackupInterval,
+                    /* userBackupManagerService */ this);
             return false;
         }
 
@@ -2392,7 +2396,8 @@
                                             + "operation; rescheduling +" + latency));
                 }
                 final long deferTime = latency;     // pin for the closure
-                FullBackupJob.schedule(mUserId, mContext, deferTime, mConstants);
+                FullBackupJob.schedule(mUserId, mContext, deferTime,
+                        /* userBackupManagerService */ this);
                 return false;
             }
 
@@ -2495,7 +2500,8 @@
         }
 
         // ...and schedule a backup pass if necessary
-        KeyValueBackupJob.schedule(mUserId, mContext, mConstants);
+        KeyValueBackupJob.schedule(mUserId, mContext,
+                /* userBackupManagerService */ this);
     }
 
     // Note: packageName is currently unused, but may be in the future
@@ -2730,7 +2736,8 @@
                                     mUserId, "Not running backup while in battery save mode"));
                 }
                 // Try again in several hours.
-                KeyValueBackupJob.schedule(mUserId, mContext, mConstants);
+                KeyValueBackupJob.schedule(mUserId, mContext,
+                        /* userBackupManagerService */ this);
             } else {
                 if (DEBUG) {
                     Slog.v(TAG, addUserIdToLogMessage(mUserId, "Scheduling immediate backup pass"));
@@ -3208,12 +3215,51 @@
         }
     }
 
+    synchronized void setFrameworkSchedulingEnabled(boolean isEnabled) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
+                "setFrameworkSchedulingEnabled");
+
+        boolean wasEnabled = isFrameworkSchedulingEnabled();
+        if (wasEnabled == isEnabled) {
+            return;
+        }
+
+        Slog.i(TAG, addUserIdToLogMessage(mUserId,
+                (isEnabled ? "Enabling" : "Disabling") + " backup scheduling"));
+
+        final long oldId = Binder.clearCallingIdentity();
+        try {
+            // TODO(b/264889098): Consider at a later point if we should us a sentinel file as
+            // setBackupEnabled.
+            Settings.Secure.putIntForUser(mContext.getContentResolver(),
+                    Settings.Secure.BACKUP_SCHEDULING_ENABLED, isEnabled ? 1 : 0, mUserId);
+
+            if (!isEnabled) {
+                KeyValueBackupJob.cancel(mUserId, mContext);
+                FullBackupJob.cancel(mUserId, mContext);
+            } else {
+                KeyValueBackupJob.schedule(mUserId, mContext, /* userBackupManagerService */ this);
+                scheduleNextFullBackupJob(/* transportMinLatency */ 0);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(oldId);
+        }
+    }
+
+    synchronized boolean isFrameworkSchedulingEnabled() {
+        // By default scheduling is enabled
+        final int defaultSetting = 1;
+        int isEnabled = Settings.Secure.getIntForUser(mContext.getContentResolver(),
+                Settings.Secure.BACKUP_SCHEDULING_ENABLED, defaultSetting, mUserId);
+        return isEnabled == 1;
+    }
+
     @VisibleForTesting
     void updateStateOnBackupEnabled(boolean wasEnabled, boolean enable) {
         synchronized (mQueueLock) {
             if (enable && !wasEnabled && mSetupComplete) {
                 // if we've just been enabled, start scheduling backup passes
-                KeyValueBackupJob.schedule(mUserId, mContext, mConstants);
+                KeyValueBackupJob.schedule(mUserId, mContext, /* userBackupManagerService */ this);
                 scheduleNextFullBackupJob(0);
             } else if (!enable) {
                 // No longer enabled, so stop running backups
@@ -4127,6 +4173,8 @@
             pw.println("Auto-restore is " + (mAutoRestore ? "enabled" : "disabled"));
             if (mBackupRunning) pw.println("Backup currently running");
             pw.println(isBackupOperationInProgress() ? "Backup in progress" : "No backups running");
+            pw.println("Framework scheduling is "
+                    + (isFrameworkSchedulingEnabled() ? "enabled" : "disabled"));
             pw.println("Last backup pass started: " + mLastBackupPass
                     + " (now = " + System.currentTimeMillis() + ')');
             pw.println("  next scheduled: " + KeyValueBackupJob.nextScheduled(mUserId));
diff --git a/services/backup/java/com/android/server/backup/internal/SetupObserver.java b/services/backup/java/com/android/server/backup/internal/SetupObserver.java
index c5e912e..f399fe9 100644
--- a/services/backup/java/com/android/server/backup/internal/SetupObserver.java
+++ b/services/backup/java/com/android/server/backup/internal/SetupObserver.java
@@ -23,7 +23,6 @@
 import android.content.Context;
 import android.database.ContentObserver;
 import android.os.Handler;
-import android.provider.Settings;
 import android.util.Slog;
 
 import com.android.server.backup.KeyValueBackupJob;
@@ -78,7 +77,7 @@
                     Slog.d(TAG, "Setup complete so starting backups");
                 }
                 KeyValueBackupJob.schedule(mUserBackupManagerService.getUserId(), mContext,
-                        mUserBackupManagerService.getConstants());
+                        mUserBackupManagerService);
                 mUserBackupManagerService.scheduleNextFullBackupJob(0);
             }
         }
diff --git a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java
index ca92b69..41e8092 100644
--- a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java
+++ b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java
@@ -1246,7 +1246,7 @@
             delay = 0;
         }
         KeyValueBackupJob.schedule(mBackupManagerService.getUserId(),
-                mBackupManagerService.getContext(), delay, mBackupManagerService.getConstants());
+                mBackupManagerService.getContext(), delay, mBackupManagerService);
 
         for (String packageName : mOriginalQueue) {
             mBackupManagerService.dataChangedImpl(packageName);
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 9752e8b..8e1f969 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -17364,6 +17364,11 @@
         }
 
         @Override
+        public Pair<Integer, Integer> getCurrentAndTargetUserIds() {
+            return mUserController.getCurrentAndTargetUserIds();
+        }
+
+        @Override
         public int getCurrentUserId() {
             return mUserController.getCurrentUserId();
         }
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 234eec3..f61737e 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -2742,6 +2742,12 @@
         return mTargetUserId != UserHandle.USER_NULL ? mTargetUserId : mCurrentUserId;
     }
 
+    Pair<Integer, Integer> getCurrentAndTargetUserIds() {
+        synchronized (mLock) {
+            return new Pair<>(mCurrentUserId, mTargetUserId);
+        }
+    }
+
     @GuardedBy("mLock")
     private int getCurrentUserIdLU() {
         return mCurrentUserId;
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index cde4ea9..ffc309e 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -64,7 +64,6 @@
 import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.server.LocalServices;
 import com.android.server.pm.UserManagerInternal;
-import com.android.server.utils.EventLogger;
 
 import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
@@ -93,8 +92,6 @@
     //       in MediaRouter2, remove this constant and replace the usages with the real request IDs.
     private static final long DUMMY_REQUEST_ID = -1;
 
-    private static final int DUMP_EVENTS_MAX_COUNT = 70;
-
     private static final String MEDIA_BETTER_TOGETHER_NAMESPACE = "media_better_together";
 
     private static final String KEY_SCANNING_PACKAGE_MINIMUM_IMPORTANCE =
@@ -121,9 +118,6 @@
     @GuardedBy("mLock")
     private int mCurrentActiveUserId = -1;
 
-    private final EventLogger mEventLogger =
-            new EventLogger(DUMP_EVENTS_MAX_COUNT, "MediaRouter2ServiceImpl");
-
     private final ActivityManager.OnUidImportanceListener mOnUidImportanceListener =
             (uid, importance) -> {
                 synchronized (mLock) {
@@ -689,16 +683,14 @@
             } else {
                 pw.println(indent + "  <no user records>");
             }
-            mEventLogger.dump(pw, indent);
         }
     }
 
     /* package */ void updateRunningUserAndProfiles(int newActiveUserId) {
         synchronized (mLock) {
             if (mCurrentActiveUserId != newActiveUserId) {
-                mEventLogger.enqueue(
-                        EventLogger.StringEvent.from("switchUser",
-                                "userId: %d", newActiveUserId));
+                Slog.i(TAG, TextUtils.formatSimple(
+                        "switchUser | user: %d", newActiveUserId));
 
                 mCurrentActiveUserId = newActiveUserId;
                 // disposeUserIfNeededLocked might modify the collection, hence clone
@@ -771,8 +763,8 @@
                 obtainMessage(UserHandler::notifyRouterRegistered,
                         userRecord.mHandler, routerRecord));
 
-        mEventLogger.enqueue(EventLogger.StringEvent.from("registerRouter2",
-                "package: %s, uid: %d, pid: %d, router id: %d",
+        Slog.i(TAG, TextUtils.formatSimple(
+                "registerRouter2 | package: %s, uid: %d, pid: %d, router: %d",
                 packageName, uid, pid, routerRecord.mRouterId));
     }
 
@@ -784,12 +776,10 @@
             return;
         }
 
-        mEventLogger.enqueue(
-                EventLogger.StringEvent.from(
-                        "unregisterRouter2",
-                        "package: %s, router id: %d",
-                        routerRecord.mPackageName,
-                        routerRecord.mRouterId));
+        Slog.i(TAG, TextUtils.formatSimple(
+                "unregisterRouter2 | package: %s, router: %d",
+                routerRecord.mPackageName,
+                routerRecord.mRouterId));
 
         UserRecord userRecord = routerRecord.mUserRecord;
         userRecord.mRouterRecords.remove(routerRecord);
@@ -816,9 +806,8 @@
             return;
         }
 
-        mEventLogger.enqueue(EventLogger.StringEvent.from(
-                "setDiscoveryRequestWithRouter2",
-                "router id: %d, discovery request: %s",
+        Slog.i(TAG, TextUtils.formatSimple(
+                "setDiscoveryRequestWithRouter2 | router: %d, discovery request: %s",
                 routerRecord.mRouterId, discoveryRequest.toString()));
 
         routerRecord.mDiscoveryPreference = discoveryRequest;
@@ -842,12 +831,11 @@
                                 .map(RouteListingPreference.Item::getRouteId)
                                 .collect(Collectors.joining(","))
                         : null;
-        mEventLogger.enqueue(
-                EventLogger.StringEvent.from(
-                        "setRouteListingPreference",
-                        "router id: %d, route listing preference: [%s]",
-                        routerRecord.mRouterId,
-                        routeListingAsString));
+
+        Slog.i(TAG, TextUtils.formatSimple(
+                "setRouteListingPreference | router: %d, route listing preference: [%s]",
+                routerRecord.mRouterId,
+                routeListingAsString));
 
         routerRecord.mUserRecord.mHandler.sendMessage(
                 obtainMessage(
@@ -863,9 +851,8 @@
         RouterRecord routerRecord = mAllRouterRecords.get(binder);
 
         if (routerRecord != null) {
-            mEventLogger.enqueue(EventLogger.StringEvent.from(
-                    "setRouteVolumeWithRouter2",
-                    "router id: %d, volume: %d",
+            Slog.i(TAG, TextUtils.formatSimple(
+                    "setRouteVolumeWithRouter2 | router: %d, volume: %d",
                     routerRecord.mRouterId, volume));
 
             routerRecord.mUserRecord.mHandler.sendMessage(
@@ -948,9 +935,8 @@
             return;
         }
 
-        mEventLogger.enqueue(EventLogger.StringEvent.from(
-                "selectRouteWithRouter2",
-                "router id: %d, route: %s",
+        Slog.i(TAG, TextUtils.formatSimple(
+                "selectRouteWithRouter2 | router: %d, route: %s",
                 routerRecord.mRouterId, route.getId()));
 
         routerRecord.mUserRecord.mHandler.sendMessage(
@@ -968,9 +954,8 @@
             return;
         }
 
-        mEventLogger.enqueue(EventLogger.StringEvent.from(
-                "deselectRouteWithRouter2",
-                "router id: %d, route: %s",
+        Slog.i(TAG, TextUtils.formatSimple(
+                "deselectRouteWithRouter2 | router: %d, route: %s",
                 routerRecord.mRouterId, route.getId()));
 
         routerRecord.mUserRecord.mHandler.sendMessage(
@@ -988,9 +973,8 @@
             return;
         }
 
-        mEventLogger.enqueue(EventLogger.StringEvent.from(
-                "transferToRouteWithRouter2",
-                "router id: %d, route: %s",
+        Slog.i(TAG, TextUtils.formatSimple(
+                "transferToRouteWithRouter2 | router: %d, route: %s",
                 routerRecord.mRouterId, route.getId()));
 
         String defaultRouteId =
@@ -1018,9 +1002,8 @@
             return;
         }
 
-        mEventLogger.enqueue(EventLogger.StringEvent.from(
-                "setSessionVolumeWithRouter2",
-                "router id: %d, session: %s, volume: %d",
+        Slog.i(TAG, TextUtils.formatSimple(
+                "setSessionVolumeWithRouter2 | router: %d, session: %s, volume: %d",
                 routerRecord.mRouterId,  uniqueSessionId, volume));
 
         routerRecord.mUserRecord.mHandler.sendMessage(
@@ -1038,9 +1021,8 @@
             return;
         }
 
-        mEventLogger.enqueue(EventLogger.StringEvent.from(
-                "releaseSessionWithRouter2",
-                "router id: %d, session: %s",
+        Slog.i(TAG, TextUtils.formatSimple(
+                "releaseSessionWithRouter2 | router: %d, session: %s",
                 routerRecord.mRouterId,  uniqueSessionId));
 
         routerRecord.mUserRecord.mHandler.sendMessage(
@@ -1084,10 +1066,9 @@
             return;
         }
 
-        mEventLogger.enqueue(
-                EventLogger.StringEvent.from("registerManager",
-                        "uid: %d, pid: %d, package: %s, userId: %d",
-                        uid, pid, packageName, userId));
+        Slog.i(TAG, TextUtils.formatSimple(
+                "registerManager | uid: %d, pid: %d, package: %s, user: %d",
+                uid, pid, packageName, userId));
 
         mContext.enforcePermission(Manifest.permission.MEDIA_CONTENT_CONTROL, pid, uid,
                 "Must hold MEDIA_CONTENT_CONTROL permission.");
@@ -1135,13 +1116,11 @@
         }
         UserRecord userRecord = managerRecord.mUserRecord;
 
-        mEventLogger.enqueue(
-                EventLogger.StringEvent.from(
-                        "unregisterManager",
-                        "package: %s, userId: %d, managerId: %d",
-                        managerRecord.mPackageName,
-                        userRecord.mUserId,
-                        managerRecord.mManagerId));
+        Slog.i(TAG, TextUtils.formatSimple(
+                "unregisterManager | package: %s, user: %d, manager: %d",
+                managerRecord.mPackageName,
+                userRecord.mUserId,
+                managerRecord.mManagerId));
 
         userRecord.mManagerRecords.remove(managerRecord);
         managerRecord.dispose();
@@ -1155,9 +1134,8 @@
             return;
         }
 
-        mEventLogger.enqueue(
-                EventLogger.StringEvent.from("startScan",
-                        "manager: %d", managerRecord.mManagerId));
+        Slog.i(TAG, TextUtils.formatSimple(
+                "startScan | manager: %d", managerRecord.mManagerId));
 
         managerRecord.startScan();
     }
@@ -1169,9 +1147,8 @@
             return;
         }
 
-        mEventLogger.enqueue(
-                EventLogger.StringEvent.from("stopScan",
-                        "manager: %d", managerRecord.mManagerId));
+        Slog.i(TAG, TextUtils.formatSimple(
+                "stopScan | manager: %d", managerRecord.mManagerId));
 
         managerRecord.stopScan();
     }
@@ -1186,10 +1163,9 @@
             return;
         }
 
-        mEventLogger.enqueue(
-                EventLogger.StringEvent.from("setRouteVolumeWithManager",
-                        "managerId: %d, routeId: %s, volume: %d",
-                        managerRecord.mManagerId, route.getId(), volume));
+        Slog.i(TAG, TextUtils.formatSimple(
+                "setRouteVolumeWithManager | manager: %d, route: %s, volume: %d",
+                managerRecord.mManagerId, route.getId(), volume));
 
         long uniqueRequestId = toUniqueRequestId(managerRecord.mManagerId, requestId);
         managerRecord.mUserRecord.mHandler.sendMessage(
@@ -1206,10 +1182,9 @@
             return;
         }
 
-        mEventLogger.enqueue(
-                EventLogger.StringEvent.from("requestCreateSessionWithManager",
-                        "managerId: %d, routeId: %s",
-                        managerRecord.mManagerId, route.getId()));
+        Slog.i(TAG, TextUtils.formatSimple(
+                "requestCreateSessionWithManager | manager: %d, route: %s",
+                managerRecord.mManagerId, route.getId()));
 
         String packageName = oldSession.getClientPackageName();
 
@@ -1256,10 +1231,9 @@
             return;
         }
 
-        mEventLogger.enqueue(
-                EventLogger.StringEvent.from("selectRouteWithManager",
-                        "managerId: %d, session: %s, routeId: %s",
-                        managerRecord.mManagerId, uniqueSessionId, route.getId()));
+        Slog.i(TAG, TextUtils.formatSimple(
+                "selectRouteWithManager | manager: %d, session: %s, route: %s",
+                managerRecord.mManagerId, uniqueSessionId, route.getId()));
 
         // Can be null if the session is system's or RCN.
         RouterRecord routerRecord = managerRecord.mUserRecord.mHandler
@@ -1282,10 +1256,9 @@
             return;
         }
 
-        mEventLogger.enqueue(
-                EventLogger.StringEvent.from("deselectRouteWithManager",
-                        "managerId: %d, session: %s, routeId: %s",
-                        managerRecord.mManagerId, uniqueSessionId, route.getId()));
+        Slog.i(TAG, TextUtils.formatSimple(
+                "deselectRouteWithManager | manager: %d, session: %s, route: %s",
+                managerRecord.mManagerId, uniqueSessionId, route.getId()));
 
         // Can be null if the session is system's or RCN.
         RouterRecord routerRecord = managerRecord.mUserRecord.mHandler
@@ -1308,10 +1281,9 @@
             return;
         }
 
-        mEventLogger.enqueue(
-                EventLogger.StringEvent.from("transferToRouteWithManager",
-                        "managerId: %d, session: %s, routeId: %s",
-                        managerRecord.mManagerId, uniqueSessionId, route.getId()));
+        Slog.i(TAG, TextUtils.formatSimple(
+                "transferToRouteWithManager | manager: %d, session: %s, route: %s",
+                managerRecord.mManagerId, uniqueSessionId, route.getId()));
 
         // Can be null if the session is system's or RCN.
         RouterRecord routerRecord = managerRecord.mUserRecord.mHandler
@@ -1334,10 +1306,9 @@
             return;
         }
 
-        mEventLogger.enqueue(
-                EventLogger.StringEvent.from("setSessionVolumeWithManager",
-                        "managerId: %d, session: %s, volume: %d",
-                        managerRecord.mManagerId, uniqueSessionId, volume));
+        Slog.i(TAG, TextUtils.formatSimple(
+                "setSessionVolumeWithManager | manager: %d, session: %s, volume: %d",
+                managerRecord.mManagerId, uniqueSessionId, volume));
 
         long uniqueRequestId = toUniqueRequestId(managerRecord.mManagerId, requestId);
         managerRecord.mUserRecord.mHandler.sendMessage(
@@ -1355,10 +1326,9 @@
             return;
         }
 
-        mEventLogger.enqueue(
-                EventLogger.StringEvent.from("releaseSessionWithManager",
-                        "managerId: %d, session: %s",
-                        managerRecord.mManagerId, uniqueSessionId));
+        Slog.i(TAG, TextUtils.formatSimple(
+                "releaseSessionWithManager | manager: %d, session: %s",
+                managerRecord.mManagerId, uniqueSessionId));
 
         RouterRecord routerRecord = managerRecord.mUserRecord.mHandler
                 .findRouterWithSessionLocked(uniqueSessionId);
@@ -1791,8 +1761,7 @@
             MediaRoute2ProviderInfo oldInfo =
                     providerInfoIndex == -1 ? null : mLastProviderInfos.get(providerInfoIndex);
             MediaRouter2ServiceImpl mediaRouter2Service = mServiceRef.get();
-            EventLogger eventLogger =
-                    mediaRouter2Service != null ? mediaRouter2Service.mEventLogger : null;
+
             if (oldInfo == newInfo) {
                 // Nothing to do.
                 return;
@@ -1854,23 +1823,21 @@
                 }
             }
 
-            if (eventLogger != null) {
-                if (!addedRoutes.isEmpty()) {
-                    // If routes were added, newInfo cannot be null.
-                    eventLogger.enqueue(
-                            toLoggingEvent(
-                                    /* source= */ "addProviderRoutes",
-                                    newInfo.getUniqueId(),
-                                    addedRoutes));
-                }
-                if (!removedRoutes.isEmpty()) {
-                    // If routes were removed, oldInfo cannot be null.
-                    eventLogger.enqueue(
-                            toLoggingEvent(
-                                    /* source= */ "removeProviderRoutes",
-                                    oldInfo.getUniqueId(),
-                                    removedRoutes));
-                }
+            if (!addedRoutes.isEmpty()) {
+                // If routes were added, newInfo cannot be null.
+                Slog.i(TAG,
+                        toLoggingMessage(
+                                /* source= */ "addProviderRoutes",
+                                newInfo.getUniqueId(),
+                                addedRoutes));
+            }
+            if (!removedRoutes.isEmpty()) {
+                // If routes were removed, oldInfo cannot be null.
+                Slog.i(TAG,
+                        toLoggingMessage(
+                                /* source= */ "removeProviderRoutes",
+                                oldInfo.getUniqueId(),
+                                removedRoutes));
             }
 
             dispatchUpdates(
@@ -1880,14 +1847,14 @@
                     mSystemProvider.getDefaultRoute());
         }
 
-        private static EventLogger.Event toLoggingEvent(
+        private static String toLoggingMessage(
                 String source, String providerId, ArrayList<MediaRoute2Info> routes) {
             String routesString =
                     routes.stream()
                             .map(it -> String.format("%s | %s", it.getOriginalId(), it.getName()))
                             .collect(Collectors.joining(/* delimiter= */ ", "));
-            return EventLogger.StringEvent.from(
-                    source, "provider: %s, routes: [%s]", providerId, routesString);
+            return TextUtils.formatSimple("%s | provider: %s, routes: [%s]",
+                    source, providerId, routesString);
         }
 
         /**
diff --git a/services/core/java/com/android/server/pm/IPackageManagerBase.java b/services/core/java/com/android/server/pm/IPackageManagerBase.java
index 38efc10..d4e3549 100644
--- a/services/core/java/com/android/server/pm/IPackageManagerBase.java
+++ b/services/core/java/com/android/server/pm/IPackageManagerBase.java
@@ -255,6 +255,12 @@
 
     @Override
     @Deprecated
+    public final void clearPersistentPreferredActivity(IntentFilter filter, int userId) {
+        mPreferredActivityHelper.clearPersistentPreferredActivity(filter, userId);
+    }
+
+    @Override
+    @Deprecated
     public final void clearPackagePreferredActivities(String packageName) {
         mPreferredActivityHelper.clearPackagePreferredActivities(snapshot(),
                 packageName);
diff --git a/services/core/java/com/android/server/pm/PreferredActivityHelper.java b/services/core/java/com/android/server/pm/PreferredActivityHelper.java
index 6f8995c..214a8b8 100644
--- a/services/core/java/com/android/server/pm/PreferredActivityHelper.java
+++ b/services/core/java/com/android/server/pm/PreferredActivityHelper.java
@@ -431,6 +431,23 @@
         }
     }
 
+    public void clearPersistentPreferredActivity(IntentFilter filter, int userId) {
+        int callingUid = Binder.getCallingUid();
+        if (callingUid != Process.SYSTEM_UID) {
+            throw new SecurityException(
+                    "clearPersistentPreferredActivity can only be run by the system");
+        }
+        boolean changed = false;
+        synchronized (mPm.mLock) {
+            changed = mPm.mSettings.clearPersistentPreferredActivity(filter, userId);
+        }
+        if (changed) {
+            updateDefaultHomeNotLocked(mPm.snapshotComputer(), userId);
+            mPm.postPreferredActivityChangedBroadcast(userId);
+            mPm.scheduleWritePackageRestrictions(userId);
+        }
+    }
+
     private boolean isHomeFilter(@NonNull WatchedIntentFilter filter) {
         return filter.hasAction(Intent.ACTION_MAIN) && filter.hasCategory(Intent.CATEGORY_HOME)
                 && filter.hasCategory(CATEGORY_DEFAULT);
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index aedf782..920fafd 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -99,6 +99,7 @@
 import com.android.modules.utils.TypedXmlSerializer;
 import com.android.permission.persistence.RuntimePermissionsPersistence;
 import com.android.permission.persistence.RuntimePermissionsState;
+import com.android.server.IntentResolver;
 import com.android.server.LocalServices;
 import com.android.server.backup.PreferredActivityBackupHelper;
 import com.android.server.pm.Installer.InstallerException;
@@ -6278,6 +6279,24 @@
         return changed;
     }
 
+    boolean clearPersistentPreferredActivity(IntentFilter filter, int userId) {
+        PersistentPreferredIntentResolver ppir = mPersistentPreferredActivities.get(userId);
+        Iterator<PersistentPreferredActivity> it = ppir.filterIterator();
+        boolean changed = false;
+        while (it.hasNext()) {
+            PersistentPreferredActivity ppa = it.next();
+            if (IntentResolver.filterEquals(ppa.getIntentFilter(), filter)) {
+                ppir.removeFilter(ppa);
+                changed = true;
+                break;
+            }
+        }
+        if (changed) {
+            onChanged();
+        }
+        return changed;
+    }
+
     ArrayList<Integer> systemReady(ComponentResolver resolver) {
         // Verify that all of the preferred activity components actually
         // exist.  It is possible for applications to be updated and at
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 175c11d..372d0aa 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -101,6 +101,7 @@
 import android.util.AtomicFile;
 import android.util.IndentingPrintWriter;
 import android.util.IntArray;
+import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
@@ -1835,6 +1836,27 @@
     }
 
     /**
+     * Gets the current and target user ids as a {@link Pair}, calling
+     * {@link ActivityManagerInternal} directly (and without performing any permission check).
+     *
+     * @return ids of current foreground user and the target user. Target user will be
+     * {@link UserHandle#USER_NULL} if there is not an ongoing user switch. And if
+     * {@link ActivityManagerInternal} is not available yet, they will both be
+     * {@link UserHandle#USER_NULL}.
+     */
+    @VisibleForTesting
+    @NonNull
+    Pair<Integer, Integer> getCurrentAndTargetUserIds() {
+        ActivityManagerInternal activityManagerInternal = getActivityManagerInternal();
+        if (activityManagerInternal == null) {
+            Slog.w(LOG_TAG, "getCurrentAndTargetUserId() called too early, "
+                    + "ActivityManagerInternal is not set yet");
+            return new Pair<>(UserHandle.USER_NULL, UserHandle.USER_NULL);
+        }
+        return activityManagerInternal.getCurrentAndTargetUserIds();
+    }
+
+    /**
      * Gets the current user id, calling {@link ActivityManagerInternal} directly (and without
      * performing any permission check).
      *
@@ -5426,11 +5448,15 @@
         final long ident = Binder.clearCallingIdentity();
         try {
             final UserData userData;
-            int currentUser = getCurrentUserId();
-            if (currentUser == userId) {
+            Pair<Integer, Integer> currentAndTargetUserIds = getCurrentAndTargetUserIds();
+            if (userId == currentAndTargetUserIds.first) {
                 Slog.w(LOG_TAG, "Current user cannot be removed.");
                 return false;
             }
+            if (userId == currentAndTargetUserIds.second) {
+                Slog.w(LOG_TAG, "Target user of an ongoing user switch cannot be removed.");
+                return false;
+            }
             synchronized (mPackagesLock) {
                 synchronized (mUsersLock) {
                     userData = mUsers.get(userId);
@@ -5567,9 +5593,10 @@
                     }
                 }
 
-                // Attempt to immediately remove a non-current user
-                final int currentUser = getCurrentUserId();
-                if (currentUser != userId) {
+                // Attempt to immediately remove a non-current and non-target user
+                Pair<Integer, Integer> currentAndTargetUserIds = getCurrentAndTargetUserIds();
+                if (userId != currentAndTargetUserIds.first
+                        && userId != currentAndTargetUserIds.second) {
                     // Attempt to remove the user. This will fail if the user is the current user
                     if (removeUserWithProfilesUnchecked(userId)) {
                         return UserManager.REMOVE_RESULT_REMOVED;
@@ -5578,9 +5605,14 @@
                 // If the user was not immediately removed, make sure it is marked as ephemeral.
                 // Don't mark as disabled since, per UserInfo.FLAG_DISABLED documentation, an
                 // ephemeral user should only be marked as disabled when its removal is in progress.
-                Slog.i(LOG_TAG, "Unable to immediately remove user " + userId + " (current user is "
-                        + currentUser + "). User is set as ephemeral and will be removed on user "
-                        + "switch or reboot.");
+                Slog.i(LOG_TAG, TextUtils.formatSimple("Unable to immediately remove user %d "
+                                + "(%s is %d). User is set as ephemeral and will be removed on "
+                                + "user switch or reboot.",
+                        userId,
+                        userId == currentAndTargetUserIds.first
+                                ? "current user"
+                                : "target user of an ongoing user switch",
+                        userId));
                 userData.info.flags |= UserInfo.FLAG_EPHEMERAL;
                 writeUserLP(userData);
 
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index 0fd6d9b..05b3ce7 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -217,4 +217,12 @@
      * @see com.android.internal.statusbar.IStatusBar#enterStageSplitFromRunningApp
      */
     void enterStageSplitFromRunningApp(boolean leftOrTop);
+
+    /**
+     * Shows the media output switcher dialog.
+     *
+     * @param packageName of the session for which the output switcher is shown.
+     * @see com.android.internal.statusbar.IStatusBar#showMediaOutputSwitcher
+     */
+    void showMediaOutputSwitcher(String packageName);
 }
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 97ca8df..0f49981 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -744,6 +744,16 @@
                 } catch (RemoteException ex) { }
             }
         }
+
+        @Override
+        public void showMediaOutputSwitcher(String packageName) {
+            if (mBar != null) {
+                try {
+                    mBar.showMediaOutputSwitcher(packageName);
+                } catch (RemoteException ex) {
+                }
+            }
+        }
     };
 
     private final GlobalActionsProvider mGlobalActionsProvider = new GlobalActionsProvider() {
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 0d6c8db..63d6509 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -3030,7 +3030,7 @@
         if (density == getInitialDisplayDensity()) {
             density = 0;
         }
-        mWmService.mDisplayWindowSettings.setForcedDensity(this, density, userId);
+        mWmService.mDisplayWindowSettings.setForcedDensity(getDisplayInfo(), density, userId);
     }
 
     /** @param mode {@link #FORCE_SCALING_MODE_AUTO} or {@link #FORCE_SCALING_MODE_DISABLED}. */
diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettings.java b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
index 6d47eeb..4c0435e 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowSettings.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
@@ -77,14 +77,14 @@
         mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
     }
 
-    void setForcedDensity(DisplayContent displayContent, int density, int userId) {
-        if (displayContent.isDefaultDisplay) {
+    void setForcedDensity(DisplayInfo info, int density, int userId) {
+        if (info.displayId == Display.DEFAULT_DISPLAY) {
             final String densityString = density == 0 ? "" : Integer.toString(density);
             Settings.Secure.putStringForUser(mService.mContext.getContentResolver(),
                     Settings.Secure.DISPLAY_DENSITY_FORCED, densityString, userId);
         }
 
-        final DisplayInfo displayInfo = displayContent.getDisplayInfo();
+        final DisplayInfo displayInfo = info;
         final SettingsProvider.SettingsEntry overrideSettings =
                 mSettingsProvider.getOverrideSettings(displayInfo);
         overrideSettings.mForcedDensity = density;
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 5adae41..18ad43c 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -5855,6 +5855,11 @@
             if (displayContent != null && displayContent.hasAccess(Binder.getCallingUid())) {
                 return displayContent.getInitialDisplayDensity();
             }
+
+            DisplayInfo info = mDisplayManagerInternal.getDisplayInfo(displayId);
+            if (info != null && info.hasAccess(Binder.getCallingUid())) {
+                return info.logicalDensityDpi;
+            }
         }
         return -1;
     }
@@ -5901,6 +5906,11 @@
                 final DisplayContent displayContent = mRoot.getDisplayContent(displayId);
                 if (displayContent != null) {
                     displayContent.setForcedDensity(density, targetUserId);
+                } else {
+                    DisplayInfo info = mDisplayManagerInternal.getDisplayInfo(displayId);
+                    if (info != null) {
+                        mDisplayWindowSettings.setForcedDensity(info, density, userId);
+                    }
                 }
             }
         } finally {
@@ -5925,6 +5935,12 @@
                 if (displayContent != null) {
                     displayContent.setForcedDensity(displayContent.getInitialDisplayDensity(),
                             callingUserId);
+                } else {
+                    DisplayInfo info = mDisplayManagerInternal.getDisplayInfo(displayId);
+                    if (info != null) {
+                        mDisplayWindowSettings.setForcedDensity(info, info.logicalDensityDpi,
+                                userId);
+                    }
                 }
             }
         } finally {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/ComponentNamePolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/ComponentNamePolicySerializer.java
new file mode 100644
index 0000000..d400000
--- /dev/null
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/ComponentNamePolicySerializer.java
@@ -0,0 +1,60 @@
+/*
+ * 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.server.devicepolicy;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.util.Log;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import java.io.IOException;
+import java.util.Objects;
+
+final class ComponentNamePolicySerializer extends PolicySerializer<ComponentName> {
+    private static final String ATTR_PACKAGE_NAME = ":package-name";
+    private static final String ATTR_CLASS_NAME = ":class-name";
+
+    @Override
+    void saveToXml(
+            TypedXmlSerializer serializer, String attributeNamePrefix, @NonNull ComponentName value)
+            throws IOException {
+        Objects.requireNonNull(value);
+        serializer.attribute(
+                /* namespace= */ null,
+                attributeNamePrefix + ATTR_PACKAGE_NAME, value.getPackageName());
+        serializer.attribute(
+                /* namespace= */ null,
+                attributeNamePrefix + ATTR_CLASS_NAME, value.getClassName());
+    }
+
+    @Nullable
+    @Override
+    ComponentName readFromXml(TypedXmlPullParser parser, String attributeNamePrefix) {
+        String packageName = parser.getAttributeValue(
+                /* namespace= */ null, attributeNamePrefix + ATTR_PACKAGE_NAME);
+        String className = parser.getAttributeValue(
+                /* namespace= */ null, attributeNamePrefix + ATTR_CLASS_NAME);
+        if (packageName == null || className == null) {
+            Log.e(DevicePolicyEngine.TAG, "Error parsing ComponentName policy.");
+            return null;
+        }
+        return new ComponentName(packageName, className);
+    }
+}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DefaultPolicyKey.java b/services/devicepolicy/java/com/android/server/devicepolicy/DefaultPolicyKey.java
new file mode 100644
index 0000000..ab0fc99
--- /dev/null
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DefaultPolicyKey.java
@@ -0,0 +1,42 @@
+/*
+ * 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.server.devicepolicy;
+
+import com.android.modules.utils.TypedXmlPullParser;
+
+/**
+ * Default implementation for {@link PolicyKey} used to identify a policy that doesn't require any
+ * additional arguments to be represented in the policy engine's data structure.
+ */
+final class DefaultPolicyKey extends PolicyKey {
+    private static final String ATTR_GENERIC_POLICY_KEY = "generic-policy-key";
+
+    DefaultPolicyKey(String policyKey) {
+        super(policyKey);
+    }
+
+    String getKey() {
+        return mKey;
+    }
+
+    static DefaultPolicyKey readGenericPolicyKeyFromXml(TypedXmlPullParser parser) {
+        String genericPolicyKey = parser.getAttributeValue(
+                /* namespace= */ null, ATTR_GENERIC_POLICY_KEY);
+        return new DefaultPolicyKey(genericPolicyKey);
+    }
+
+}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java
index 7ec809f..cb3b021 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java
@@ -16,14 +16,10 @@
 
 package com.android.server.devicepolicy;
 
-import static android.app.admin.PolicyUpdateReason.REASON_CONFLICTING_ADMIN_POLICY;
-import static android.app.admin.PolicyUpdatesReceiver.EXTRA_POLICY_BUNDLE_KEY;
-import static android.app.admin.PolicyUpdatesReceiver.EXTRA_POLICY_KEY;
-import static android.app.admin.PolicyUpdatesReceiver.EXTRA_POLICY_SET_RESULT_KEY;
+import static android.app.admin.PolicyUpdateResult.RESULT_FAILURE_CONFLICTING_ADMIN_POLICY;
+import static android.app.admin.PolicyUpdateResult.RESULT_SUCCESS;
 import static android.app.admin.PolicyUpdatesReceiver.EXTRA_POLICY_TARGET_USER_ID;
-import static android.app.admin.PolicyUpdatesReceiver.EXTRA_POLICY_UPDATE_REASON_KEY;
-import static android.app.admin.PolicyUpdatesReceiver.POLICY_SET_RESULT_FAILURE;
-import static android.app.admin.PolicyUpdatesReceiver.POLICY_SET_RESULT_SUCCESS;
+import static android.app.admin.PolicyUpdatesReceiver.EXTRA_POLICY_UPDATE_RESULT_KEY;
 
 import android.Manifest;
 import android.annotation.NonNull;
@@ -82,12 +78,12 @@
     /**
      * Map of <userId, Map<policyKey, policyState>>
      */
-    private final SparseArray<Map<String, PolicyState<?>>> mLocalPolicies;
+    private final SparseArray<Map<PolicyKey, PolicyState<?>>> mLocalPolicies;
 
     /**
      * Map of <policyKey, policyState>
      */
-    private final Map<String, PolicyState<?>> mGlobalPolicies;
+    private final Map<PolicyKey, PolicyState<?>> mGlobalPolicies;
 
     /**
      * Map containing the current set of admins in each user with active policies.
@@ -146,9 +142,8 @@
             sendPolicyResultToAdmin(
                     enforcingAdmin,
                     policyDefinition,
-                    policyEnforced,
                     // TODO: we're always sending this for now, should properly handle errors.
-                    REASON_CONFLICTING_ADMIN_POLICY,
+                    policyEnforced ? RESULT_SUCCESS : RESULT_FAILURE_CONFLICTING_ADMIN_POLICY,
                     userId);
 
             updateDeviceAdminServiceOnPolicyAddLocked(enforcingAdmin);
@@ -194,9 +189,8 @@
             sendPolicyResultToAdmin(
                     enforcingAdmin,
                     policyDefinition,
-                    policyEnforced,
                     // TODO: we're always sending this for now, should properly handle errors.
-                    REASON_CONFLICTING_ADMIN_POLICY,
+                    policyEnforced ? RESULT_SUCCESS : RESULT_FAILURE_CONFLICTING_ADMIN_POLICY,
                     userId);
 
             if (localPolicyState.getPoliciesSetByAdmins().isEmpty()) {
@@ -223,7 +217,7 @@
 
         // Send policy updates to admins who've set it locally
         sendPolicyChangedToAdmins(
-                localPolicyState.getPoliciesSetByAdmins().keySet(),
+                localPolicyState,
                 enforcingAdmin,
                 policyDefinition,
                 // This policy change is only relevant to a single user, not the global
@@ -234,7 +228,7 @@
         if (hasGlobalPolicyLocked(policyDefinition)) {
             PolicyState<V> globalPolicyState = getGlobalPolicyStateLocked(policyDefinition);
             sendPolicyChangedToAdmins(
-                    globalPolicyState.getPoliciesSetByAdmins().keySet(),
+                    globalPolicyState,
                     enforcingAdmin,
                     policyDefinition,
                     userId);
@@ -266,13 +260,13 @@
                     policyDefinition, enforcingAdmin, value);
             boolean policyEnforcedGlobally = Objects.equals(
                     globalPolicyState.getCurrentResolvedPolicy(), value);
+            boolean policyEnforced = policyEnforcedGlobally && policyEnforcedOnAllUsers;
 
             sendPolicyResultToAdmin(
                     enforcingAdmin,
                     policyDefinition,
-                    policyEnforcedGlobally && policyEnforcedOnAllUsers,
                     // TODO: we're always sending this for now, should properly handle errors.
-                    REASON_CONFLICTING_ADMIN_POLICY,
+                    policyEnforced ? RESULT_SUCCESS : RESULT_FAILURE_CONFLICTING_ADMIN_POLICY,
                     UserHandle.USER_ALL);
 
             updateDeviceAdminServiceOnPolicyAddLocked(enforcingAdmin);
@@ -305,13 +299,13 @@
                     policyDefinition, enforcingAdmin, /* value= */ null);
             // For a removePolicy to be enforced, it means no current policy exists
             boolean policyEnforcedGlobally = policyState.getCurrentResolvedPolicy() == null;
+            boolean policyEnforced = policyEnforcedGlobally && policyEnforcedOnAllUsers;
 
             sendPolicyResultToAdmin(
                     enforcingAdmin,
                     policyDefinition,
-                    policyEnforcedGlobally && policyEnforcedOnAllUsers,
                     // TODO: we're always sending this for now, should properly handle errors.
-                    REASON_CONFLICTING_ADMIN_POLICY,
+                    policyEnforced ? RESULT_SUCCESS : RESULT_FAILURE_CONFLICTING_ADMIN_POLICY,
                     UserHandle.USER_ALL);
 
             if (policyState.getPoliciesSetByAdmins().isEmpty()) {
@@ -336,7 +330,7 @@
                 UserHandle.USER_ALL);
 
         sendPolicyChangedToAdmins(
-                policyState.getPoliciesSetByAdmins().keySet(),
+                policyState,
                 enforcingAdmin,
                 policyDefinition,
                 UserHandle.USER_ALL);
@@ -376,7 +370,7 @@
                 enforcePolicy(
                         policyDefinition, localPolicyState.getCurrentResolvedPolicy(), userId);
                 sendPolicyChangedToAdmins(
-                        localPolicyState.getPoliciesSetByAdmins().keySet(),
+                        localPolicyState,
                         enforcingAdmin,
                         policyDefinition,
                         // Even though this is caused by a global policy change, admins who've set
@@ -430,6 +424,42 @@
         }
     }
 
+    /**
+     * Returns the policies set by the given admin that share the same {@link PolicyKey#getKey()} as
+     * the provided {@code policyDefinition}.
+     *
+     * <p>For example, getLocalPolicyKeysSetByAdmin(PERMISSION_GRANT, admin) returns all permission
+     * grants set by the given admin.
+     *
+     * <p>Note that this will always return at most one item for policies that do not require
+     * additional params (e.g. {@link PolicyDefinition#LOCK_TASK} vs
+     * {@link PolicyDefinition#PERMISSION_GRANT(String, String)}).
+     *
+     */
+    @NonNull
+    <V> Set<PolicyKey> getLocalPolicyKeysSetByAdmin(
+            @NonNull PolicyDefinition<V> policyDefinition,
+            @NonNull EnforcingAdmin enforcingAdmin,
+            int userId) {
+        Objects.requireNonNull(policyDefinition);
+        Objects.requireNonNull(enforcingAdmin);
+
+        synchronized (mLock) {
+            if (policyDefinition.isGlobalOnlyPolicy() || !mLocalPolicies.contains(userId)) {
+                return Set.of();
+            }
+            Set<PolicyKey> keys = new HashSet<>();
+            for (PolicyKey key : mLocalPolicies.get(userId).keySet()) {
+                if (key.hasSameKeyAs(policyDefinition.getPolicyKey())
+                        && mLocalPolicies.get(userId).get(key).getPoliciesSetByAdmins()
+                        .containsKey(enforcingAdmin)) {
+                    keys.add(key);
+                }
+            }
+            return keys;
+        }
+    }
+
     private <V> boolean hasLocalPolicyLocked(PolicyDefinition<V> policyDefinition, int userId) {
         if (policyDefinition.isGlobalOnlyPolicy()) {
             return false;
@@ -501,7 +531,7 @@
     }
 
     private static <V> PolicyState<V> getPolicyState(
-            Map<String, PolicyState<?>> policies, PolicyDefinition<V> policyDefinition) {
+            Map<PolicyKey, PolicyState<?>> policies, PolicyDefinition<V> policyDefinition) {
         try {
             // This will not throw an exception because policyDefinition is of type V, so unless
             // we've created two policies with the same key but different types - we can only have
@@ -523,8 +553,7 @@
     }
 
     private <V> void sendPolicyResultToAdmin(
-            EnforcingAdmin admin, PolicyDefinition<V> policyDefinition, boolean success,
-            int reason, int userId) {
+            EnforcingAdmin admin, PolicyDefinition<V> policyDefinition, int result, int userId) {
         Intent intent = new Intent(PolicyUpdatesReceiver.ACTION_DEVICE_POLICY_SET_RESULT);
         intent.setPackage(admin.getPackageName());
 
@@ -539,21 +568,14 @@
         }
 
         Bundle extras = new Bundle();
-        extras.putString(EXTRA_POLICY_KEY, policyDefinition.getPolicyDefinitionKey());
-        if (policyDefinition.getCallbackArgs() != null
-                && !policyDefinition.getCallbackArgs().isEmpty()) {
-            extras.putBundle(EXTRA_POLICY_BUNDLE_KEY, policyDefinition.getCallbackArgs());
-        }
+        policyDefinition.getPolicyKey().writeToBundle(extras);
         extras.putInt(
                 EXTRA_POLICY_TARGET_USER_ID,
                 getTargetUser(admin.getUserId(), userId));
         extras.putInt(
-                EXTRA_POLICY_SET_RESULT_KEY,
-                success ? POLICY_SET_RESULT_SUCCESS : POLICY_SET_RESULT_FAILURE);
+                EXTRA_POLICY_UPDATE_RESULT_KEY,
+                result);
 
-        if (!success) {
-            extras.putInt(EXTRA_POLICY_UPDATE_REASON_KEY, reason);
-        }
         intent.putExtras(extras);
 
         maybeSendIntentToAdminReceivers(intent, UserHandle.of(admin.getUserId()), receivers);
@@ -561,17 +583,21 @@
 
     // TODO(b/261430877): Finalise the decision on which admins to send the updates to.
     private <V> void sendPolicyChangedToAdmins(
-            Set<EnforcingAdmin> admins,
+            PolicyState<V> policyState,
             EnforcingAdmin callingAdmin,
             PolicyDefinition<V> policyDefinition,
             int userId) {
-        for (EnforcingAdmin admin: admins) {
+        for (EnforcingAdmin admin: policyState.getPoliciesSetByAdmins().keySet()) {
             // We're sending a separate broadcast for the calling admin with the result.
             if (admin.equals(callingAdmin)) {
                 continue;
             }
+            int result = Objects.equals(
+                    policyState.getPoliciesSetByAdmins().get(admin),
+                    policyState.getCurrentResolvedPolicy())
+                    ? RESULT_SUCCESS : RESULT_FAILURE_CONFLICTING_ADMIN_POLICY;
             maybeSendOnPolicyChanged(
-                    admin, policyDefinition, REASON_CONFLICTING_ADMIN_POLICY, userId);
+                    admin, policyDefinition, result, userId);
         }
     }
 
@@ -592,15 +618,11 @@
         }
 
         Bundle extras = new Bundle();
-        extras.putString(EXTRA_POLICY_KEY, policyDefinition.getPolicyDefinitionKey());
-        if (policyDefinition.getCallbackArgs() != null
-                && !policyDefinition.getCallbackArgs().isEmpty()) {
-            extras.putBundle(EXTRA_POLICY_BUNDLE_KEY, policyDefinition.getCallbackArgs());
-        }
+        policyDefinition.getPolicyKey().writeToBundle(extras);
         extras.putInt(
                 EXTRA_POLICY_TARGET_USER_ID,
                 getTargetUser(admin.getUserId(), userId));
-        extras.putInt(EXTRA_POLICY_UPDATE_REASON_KEY, reason);
+        extras.putInt(EXTRA_POLICY_UPDATE_RESULT_KEY, reason);
         intent.putExtras(extras);
         intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
 
@@ -779,14 +801,14 @@
     }
 
     private boolean doesAdminHavePolicies(@NonNull EnforcingAdmin enforcingAdmin) {
-        for (String policy : mGlobalPolicies.keySet()) {
+        for (PolicyKey policy : mGlobalPolicies.keySet()) {
             PolicyState<?> policyState = mGlobalPolicies.get(policy);
             if (policyState.getPoliciesSetByAdmins().containsKey(enforcingAdmin)) {
                 return true;
             }
         }
         for (int i = 0; i < mLocalPolicies.size(); i++) {
-            for (String policy : mLocalPolicies.get(mLocalPolicies.keyAt(i)).keySet()) {
+            for (PolicyKey policy : mLocalPolicies.get(mLocalPolicies.keyAt(i)).keySet()) {
                 PolicyState<?> policyState = mLocalPolicies.get(
                         mLocalPolicies.keyAt(i)).get(policy);
                 if (policyState.getPoliciesSetByAdmins().containsKey(enforcingAdmin)) {
@@ -880,13 +902,12 @@
             if (mLocalPolicies != null) {
                 for (int i = 0; i < mLocalPolicies.size(); i++) {
                     int userId = mLocalPolicies.keyAt(i);
-                    for (Map.Entry<String, PolicyState<?>> policy : mLocalPolicies.get(
+                    for (Map.Entry<PolicyKey, PolicyState<?>> policy : mLocalPolicies.get(
                             userId).entrySet()) {
                         serializer.startTag(/* namespace= */ null, TAG_LOCAL_POLICY_ENTRY);
 
                         serializer.attributeInt(/* namespace= */ null, ATTR_USER_ID, userId);
-                        serializer.attribute(
-                                /* namespace= */ null, ATTR_POLICY_ID, policy.getKey());
+                        policy.getKey().saveToXml(serializer);
 
                         serializer.startTag(/* namespace= */ null, TAG_ADMINS_POLICY_ENTRY);
                         policy.getValue().saveToXml(serializer);
@@ -900,10 +921,10 @@
 
         private void writeGlobalPoliciesInner(TypedXmlSerializer serializer) throws IOException {
             if (mGlobalPolicies != null) {
-                for (Map.Entry<String, PolicyState<?>> policy : mGlobalPolicies.entrySet()) {
+                for (Map.Entry<PolicyKey, PolicyState<?>> policy : mGlobalPolicies.entrySet()) {
                     serializer.startTag(/* namespace= */ null, TAG_GLOBAL_POLICY_ENTRY);
 
-                    serializer.attribute(/* namespace= */ null, ATTR_POLICY_ID, policy.getKey());
+                    policy.getKey().saveToXml(serializer);
 
                     serializer.startTag(/* namespace= */ null, TAG_ADMINS_POLICY_ENTRY);
                     policy.getValue().saveToXml(serializer);
@@ -973,8 +994,7 @@
         private void readLocalPoliciesInner(TypedXmlPullParser parser)
                 throws XmlPullParserException, IOException {
             int userId = parser.getAttributeInt(/* namespace= */ null, ATTR_USER_ID);
-            String policyKey = parser.getAttributeValue(
-                    /* namespace= */ null, ATTR_POLICY_ID);
+            PolicyKey policyKey = PolicyDefinition.readPolicyKeyFromXml(parser);
             if (!mLocalPolicies.contains(userId)) {
                 mLocalPolicies.put(userId, new HashMap<>());
             }
@@ -989,7 +1009,7 @@
 
         private void readGlobalPoliciesInner(TypedXmlPullParser parser)
                 throws IOException, XmlPullParserException {
-            String policyKey = parser.getAttributeValue(/* namespace= */ null, ATTR_POLICY_ID);
+            PolicyKey policyKey = PolicyDefinition.readPolicyKeyFromXml(parser);
             PolicyState<?> adminsPolicy = parseAdminsPolicy(parser);
             if (adminsPolicy != null) {
                 mGlobalPolicies.put(policyKey, adminsPolicy);
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index fa6fa53..ce67f3c 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -10213,16 +10213,24 @@
                 || isDefaultDeviceOwner(caller) || isFinancedDeviceOwner(caller));
 
         final int userHandle = caller.getUserId();
-        synchronized (getLockObject()) {
-            long id = mInjector.binderClearCallingIdentity();
-            try {
-                mIPackageManager.addPersistentPreferredActivity(filter, activity, userHandle);
-                mIPackageManager.flushPackageRestrictionsAsUser(userHandle);
-            } catch (RemoteException re) {
-                // Shouldn't happen
-                Slog.wtf(LOG_TAG, "Error adding persistent preferred activity", re);
-            } finally {
-                mInjector.binderRestoreCallingIdentity(id);
+        if (isCoexistenceEnabled(caller)) {
+            mDevicePolicyEngine.setLocalPolicy(
+                    PolicyDefinition.PERSISTENT_PREFERRED_ACTIVITY(filter),
+                    EnforcingAdmin.createEnterpriseEnforcingAdmin(who, userHandle),
+                    activity,
+                    userHandle);
+        } else {
+            synchronized (getLockObject()) {
+                long id = mInjector.binderClearCallingIdentity();
+                try {
+                    mIPackageManager.addPersistentPreferredActivity(filter, activity, userHandle);
+                    mIPackageManager.flushPackageRestrictionsAsUser(userHandle);
+                } catch (RemoteException re) {
+                    // Shouldn't happen
+                    Slog.wtf(LOG_TAG, "Error adding persistent preferred activity", re);
+                } finally {
+                    mInjector.binderRestoreCallingIdentity(id);
+                }
             }
         }
         final String activityPackage =
@@ -10242,17 +10250,61 @@
                 || isDefaultDeviceOwner(caller) || isFinancedDeviceOwner(caller));
 
         final int userHandle = caller.getUserId();
-        synchronized (getLockObject()) {
-            long id = mInjector.binderClearCallingIdentity();
-            try {
-                mIPackageManager.clearPackagePersistentPreferredActivities(packageName, userHandle);
-                mIPackageManager.flushPackageRestrictionsAsUser(userHandle);
-            } catch (RemoteException re) {
-                // Shouldn't happen
-                Slogf.wtf(
-                        LOG_TAG, "Error when clearing package persistent preferred activities", re);
-            } finally {
-                mInjector.binderRestoreCallingIdentity(id);
+
+        if (isCoexistenceEnabled(caller)) {
+            clearPackagePersistentPreferredActivitiesFromPolicyEngine(
+                    EnforcingAdmin.createEnterpriseEnforcingAdmin(who, userHandle),
+                    packageName,
+                    userHandle);
+        } else {
+            synchronized (getLockObject()) {
+                long id = mInjector.binderClearCallingIdentity();
+                try {
+                    mIPackageManager.clearPackagePersistentPreferredActivities(packageName,
+                            userHandle);
+                    mIPackageManager.flushPackageRestrictionsAsUser(userHandle);
+                } catch (RemoteException re) {
+                    // Shouldn't happen
+                    Slogf.wtf(
+                            LOG_TAG, "Error when clearing package persistent preferred activities",
+                            re);
+                } finally {
+                    mInjector.binderRestoreCallingIdentity(id);
+                }
+            }
+        }
+    }
+
+    /**
+     * Remove all persistent intent handler preferences associated with the given package that were
+     * set by this admin, note that is doesn't remove preferences set by other admins for the same
+     * package.
+     */
+    private void clearPackagePersistentPreferredActivitiesFromPolicyEngine(
+            EnforcingAdmin admin, String packageName, int userId) {
+        Set<PolicyKey> keys = mDevicePolicyEngine.getLocalPolicyKeysSetByAdmin(
+                PolicyDefinition.GENERIC_PERSISTENT_PREFERRED_ACTIVITY,
+                admin,
+                userId);
+        for (PolicyKey key : keys) {
+            if (!(key instanceof PersistentPreferredActivityPolicyKey)) {
+                throw new IllegalStateException("PolicyKey for PERSISTENT_PREFERRED_ACTIVITY is not"
+                        + "of type PersistentPreferredActivityPolicyKey");
+            }
+            PersistentPreferredActivityPolicyKey parsedKey =
+                    (PersistentPreferredActivityPolicyKey) key;
+            IntentFilter filter = Objects.requireNonNull(parsedKey.getFilter());
+
+            ComponentName preferredActivity = mDevicePolicyEngine.getLocalPolicySetByAdmin(
+                    PolicyDefinition.PERSISTENT_PREFERRED_ACTIVITY(filter),
+                    admin,
+                    userId);
+            if (preferredActivity != null
+                    && preferredActivity.getPackageName().equals(packageName)) {
+                mDevicePolicyEngine.removeLocalPolicy(
+                        PolicyDefinition.PERSISTENT_PREFERRED_ACTIVITY(filter),
+                        admin,
+                        userId);
             }
         }
     }
@@ -12178,24 +12230,40 @@
                 || isFinancedDeviceOwner(caller)))
                 || (caller.hasPackage() && isCallerDelegate(caller, DELEGATION_BLOCK_UNINSTALL)));
 
-        final int userId = caller.getUserId();
-        synchronized (getLockObject()) {
-            long id = mInjector.binderClearCallingIdentity();
-            try {
-                mIPackageManager.setBlockUninstallForUser(packageName, uninstallBlocked, userId);
-            } catch (RemoteException re) {
-                // Shouldn't happen.
-                Slogf.e(LOG_TAG, "Failed to setBlockUninstallForUser", re);
-            } finally {
-                mInjector.binderRestoreCallingIdentity(id);
+        if (isCoexistenceEnabled(caller)) {
+            // TODO(b/260573124): Add correct enforcing admin when permission changes are
+            //  merged, and don't forget to handle delegates! Enterprise admins assume
+            //  component name isn't null.
+            EnforcingAdmin admin = EnforcingAdmin.createEnterpriseEnforcingAdmin(
+                    who != null ? who : new ComponentName(callerPackage, "delegate"),
+                    caller.getUserId());
+            mDevicePolicyEngine.setLocalPolicy(
+                    PolicyDefinition.PACKAGE_UNINSTALL_BLOCKED(packageName),
+                    admin,
+                    uninstallBlocked,
+                    caller.getUserId());
+        } else {
+            final int userId = caller.getUserId();
+            synchronized (getLockObject()) {
+                long id = mInjector.binderClearCallingIdentity();
+                try {
+                    mIPackageManager.setBlockUninstallForUser(
+                            packageName, uninstallBlocked, userId);
+                } catch (RemoteException re) {
+                    // Shouldn't happen.
+                    Slogf.e(LOG_TAG, "Failed to setBlockUninstallForUser", re);
+                } finally {
+                    mInjector.binderRestoreCallingIdentity(id);
+                }
+            }
+            if (uninstallBlocked) {
+                final PackageManagerInternal pmi = mInjector.getPackageManagerInternal();
+                pmi.removeNonSystemPackageSuspensions(packageName, userId);
+                pmi.removeDistractingPackageRestrictions(packageName, userId);
+                pmi.flushPackageRestrictions(userId);
             }
         }
-        if (uninstallBlocked) {
-            final PackageManagerInternal pmi = mInjector.getPackageManagerInternal();
-            pmi.removeNonSystemPackageSuspensions(packageName, userId);
-            pmi.removeDistractingPackageRestrictions(packageName, userId);
-            pmi.flushPackageRestrictions(userId);
-        }
+
         DevicePolicyEventLogger
                 .createEvent(DevicePolicyEnums.SET_UNINSTALL_BLOCKED)
                 .setAdmin(caller.getPackageName())
@@ -12204,6 +12272,26 @@
                 .write();
     }
 
+    static void setUninstallBlockedUnchecked(
+            String packageName, boolean uninstallBlocked, int userId) {
+        Binder.withCleanCallingIdentity(() -> {
+            try {
+                AppGlobals.getPackageManager().setBlockUninstallForUser(
+                        packageName, uninstallBlocked, userId);
+            } catch (RemoteException re) {
+                // Shouldn't happen.
+                Slogf.e(LOG_TAG, "Failed to setBlockUninstallForUser", re);
+            }
+        });
+        if (uninstallBlocked) {
+            final PackageManagerInternal pmi = LocalServices.getService(
+                    PackageManagerInternal.class);
+            pmi.removeNonSystemPackageSuspensions(packageName, userId);
+            pmi.removeDistractingPackageRestrictions(packageName, userId);
+            pmi.flushPackageRestrictions(userId);
+        }
+    }
+
     @Override
     public boolean isUninstallBlocked(ComponentName who, String packageName) {
         // This function should return true if and only if the package is blocked by
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PackageSpecificPolicyKey.java b/services/devicepolicy/java/com/android/server/devicepolicy/PackageSpecificPolicyKey.java
new file mode 100644
index 0000000..1665830
--- /dev/null
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PackageSpecificPolicyKey.java
@@ -0,0 +1,101 @@
+/*
+ * 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.server.devicepolicy;
+
+import static android.app.admin.PolicyUpdatesReceiver.EXTRA_PACKAGE_NAME;
+import static android.app.admin.PolicyUpdatesReceiver.EXTRA_POLICY_BUNDLE_KEY;
+
+import android.annotation.Nullable;
+import android.os.Bundle;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * Class used to identify a policy that relates to a certain package in the policy engine's data
+ * structure.
+ */
+final class PackageSpecificPolicyKey extends PolicyKey {
+    private static final String ATTR_POLICY_KEY = "policy-key";
+    private static final String ATTR_PACKAGE_NAME = "package-name";
+    private static final String ATTR_PERMISSION_NAME = "permission-name";
+
+    private final String mPackageName;
+
+    PackageSpecificPolicyKey(String key, String packageName) {
+        super(key);
+        mPackageName = Objects.requireNonNull((packageName));
+    }
+
+    PackageSpecificPolicyKey(String key) {
+        super(key);
+        mPackageName = null;
+    }
+
+    @Nullable
+    String getPackageName() {
+        return mPackageName;
+    }
+
+    @Override
+    void saveToXml(TypedXmlSerializer serializer) throws IOException {
+        serializer.attribute(/* namespace= */ null, ATTR_POLICY_KEY, mKey);
+        serializer.attribute(/* namespace= */ null, ATTR_PACKAGE_NAME, mPackageName);
+    }
+
+    @Override
+    PackageSpecificPolicyKey readFromXml(TypedXmlPullParser parser)
+            throws XmlPullParserException, IOException {
+        String policyKey = parser.getAttributeValue(/* namespace= */ null, ATTR_POLICY_KEY);
+        String packageName = parser.getAttributeValue(/* namespace= */ null, ATTR_PACKAGE_NAME);
+        String permissionName = parser.getAttributeValue(
+                /* namespace= */ null, ATTR_PERMISSION_NAME);
+        return new PackageSpecificPolicyKey(policyKey, packageName);
+    }
+
+    @Override
+    void writeToBundle(Bundle bundle) {
+        super.writeToBundle(bundle);
+        Bundle extraPolicyParams = new Bundle();
+        extraPolicyParams.putString(EXTRA_PACKAGE_NAME, mPackageName);
+        bundle.putBundle(EXTRA_POLICY_BUNDLE_KEY, extraPolicyParams);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        PackageSpecificPolicyKey other = (PackageSpecificPolicyKey) o;
+        return Objects.equals(mKey, other.mKey)
+                && Objects.equals(mPackageName, other.mPackageName);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mKey, mPackageName);
+    }
+
+    @Override
+    public String toString() {
+        return "mPolicyKey= " + mKey + "; mPackageName= " + mPackageName;
+    }
+}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PermissionGrantStatePolicyKey.java b/services/devicepolicy/java/com/android/server/devicepolicy/PermissionGrantStatePolicyKey.java
new file mode 100644
index 0000000..b7d805e
--- /dev/null
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PermissionGrantStatePolicyKey.java
@@ -0,0 +1,113 @@
+/*
+ * 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.server.devicepolicy;
+
+import static android.app.admin.PolicyUpdatesReceiver.EXTRA_PACKAGE_NAME;
+import static android.app.admin.PolicyUpdatesReceiver.EXTRA_PERMISSION_NAME;
+import static android.app.admin.PolicyUpdatesReceiver.EXTRA_POLICY_BUNDLE_KEY;
+
+import android.annotation.Nullable;
+import android.os.Bundle;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * Class used to identify a PermissionGrantState policy in the policy engine's data structure.
+ */
+final class PermissionGrantStatePolicyKey extends PolicyKey {
+    private static final String ATTR_POLICY_KEY = "policy-key";
+    private static final String ATTR_PACKAGE_NAME = "package-name";
+    private static final String ATTR_PERMISSION_NAME = "permission-name";
+
+    private final String mPackageName;
+    private final String mPermissionName;
+
+    PermissionGrantStatePolicyKey(String key, String packageName, String permissionName) {
+        super(key);
+        mPackageName = Objects.requireNonNull((packageName));
+        mPermissionName = Objects.requireNonNull((permissionName));
+    }
+
+    PermissionGrantStatePolicyKey(String key) {
+        super(key);
+        mPackageName = null;
+        mPermissionName = null;
+    }
+
+    @Nullable
+    String getPackageName() {
+        return mPackageName;
+    }
+
+    @Nullable
+    String getPermissionName() {
+        return mPermissionName;
+    }
+
+    @Override
+    void saveToXml(TypedXmlSerializer serializer) throws IOException {
+        serializer.attribute(/* namespace= */ null, ATTR_POLICY_KEY, mKey);
+        serializer.attribute(/* namespace= */ null, ATTR_PACKAGE_NAME, mPackageName);
+        serializer.attribute(/* namespace= */ null, ATTR_PERMISSION_NAME, mPermissionName);
+    }
+
+    @Override
+    PermissionGrantStatePolicyKey readFromXml(TypedXmlPullParser parser)
+            throws XmlPullParserException, IOException {
+        String policyKey = parser.getAttributeValue(/* namespace= */ null, ATTR_POLICY_KEY);
+        String packageName = parser.getAttributeValue(/* namespace= */ null, ATTR_PACKAGE_NAME);
+        String permissionName = parser.getAttributeValue(
+                /* namespace= */ null, ATTR_PERMISSION_NAME);
+        return new PermissionGrantStatePolicyKey(policyKey, packageName, permissionName);
+    }
+
+    @Override
+    void writeToBundle(Bundle bundle) {
+        super.writeToBundle(bundle);
+        Bundle extraPolicyParams = new Bundle();
+        extraPolicyParams.putString(EXTRA_PACKAGE_NAME, mPackageName);
+        extraPolicyParams.putString(EXTRA_PERMISSION_NAME, mPermissionName);
+        bundle.putBundle(EXTRA_POLICY_BUNDLE_KEY, extraPolicyParams);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        PermissionGrantStatePolicyKey other = (PermissionGrantStatePolicyKey) o;
+        return Objects.equals(mKey, other.mKey)
+                && Objects.equals(mPackageName, other.mPackageName)
+                && Objects.equals(mPermissionName, other.mPermissionName);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mKey, mPackageName, mPermissionName);
+    }
+
+    @Override
+    public String toString() {
+        return "mPolicyKey= " + mKey + "; mPackageName= " + mPackageName + "; mPermissionName= "
+                + mPermissionName;
+    }
+}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PersistentPreferredActivityPolicyKey.java b/services/devicepolicy/java/com/android/server/devicepolicy/PersistentPreferredActivityPolicyKey.java
new file mode 100644
index 0000000..f8c07595
--- /dev/null
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PersistentPreferredActivityPolicyKey.java
@@ -0,0 +1,99 @@
+/*
+ * 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.server.devicepolicy;
+
+import static android.app.admin.PolicyUpdatesReceiver.EXTRA_INTENT_FILTER;
+import static android.app.admin.PolicyUpdatesReceiver.EXTRA_POLICY_BUNDLE_KEY;
+
+import android.annotation.Nullable;
+import android.content.IntentFilter;
+import android.os.Bundle;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+import com.android.server.IntentResolver;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * Class used to identify a PersistentPreferredActivity policy in the policy engine's data
+ * structure.
+ */
+final class PersistentPreferredActivityPolicyKey extends PolicyKey {
+    private static final String ATTR_POLICY_KEY = "policy-key";
+    private IntentFilter mFilter;
+
+    PersistentPreferredActivityPolicyKey(String policyKey, IntentFilter filter) {
+        super(policyKey);
+        mFilter = Objects.requireNonNull((filter));
+    }
+
+    PersistentPreferredActivityPolicyKey(String policyKey) {
+        super(policyKey);
+        mFilter = null;
+    }
+
+    @Nullable
+    IntentFilter getFilter() {
+        return mFilter;
+    }
+
+    @Override
+    void saveToXml(TypedXmlSerializer serializer) throws IOException {
+        serializer.attribute(/* namespace= */ null, ATTR_POLICY_KEY, mKey);
+        mFilter.writeToXml(serializer);
+    }
+
+    @Override
+    PersistentPreferredActivityPolicyKey readFromXml(TypedXmlPullParser parser)
+            throws XmlPullParserException, IOException {
+        String policyKey = parser.getAttributeValue(/* namespace= */ null, ATTR_POLICY_KEY);
+        IntentFilter filter = new IntentFilter();
+        filter.readFromXml(parser);
+        return new PersistentPreferredActivityPolicyKey(policyKey, filter);
+    }
+
+    @Override
+    void writeToBundle(Bundle bundle) {
+        super.writeToBundle(bundle);
+        Bundle extraPolicyParams = new Bundle();
+        extraPolicyParams.putParcelable(EXTRA_INTENT_FILTER, mFilter);
+        bundle.putBundle(EXTRA_POLICY_BUNDLE_KEY, extraPolicyParams);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        PersistentPreferredActivityPolicyKey other = (PersistentPreferredActivityPolicyKey) o;
+        return Objects.equals(mKey, other.mKey)
+                && IntentResolver.filterEquals(mFilter, other.mFilter);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mKey, mFilter);
+    }
+
+    @Override
+    public String toString() {
+        return "mKey= " + mKey + "; mFilter= " + mFilter;
+    }
+}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
index cfb3db0..ab1658f 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
@@ -19,9 +19,9 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.admin.DevicePolicyManager;
-import android.app.admin.PolicyUpdatesReceiver;
+import android.content.ComponentName;
 import android.content.Context;
-import android.os.Bundle;
+import android.content.IntentFilter;
 
 import com.android.internal.util.function.QuadFunction;
 import com.android.modules.utils.TypedXmlPullParser;
@@ -47,27 +47,25 @@
     private static final MostRestrictive<Boolean> FALSE_MORE_RESTRICTIVE = new MostRestrictive<>(
             List.of(false, true));
 
-    private static final String ATTR_POLICY_KEY = "policy-key";
-    private static final String ATTR_POLICY_DEFINITION_KEY = "policy-type-key";
-    private static final String ATTR_CALLBACK_ARGS_SIZE = "size";
-    private static final String ATTR_CALLBACK_ARGS_KEY = "key";
-    private static final String ATTR_CALLBACK_ARGS_VALUE = "value";
-
+    private static final MostRestrictive<Boolean> TRUE_MORE_RESTRICTIVE = new MostRestrictive<>(
+            List.of(true, false));
 
     static PolicyDefinition<Boolean> AUTO_TIMEZONE = new PolicyDefinition<>(
-            DevicePolicyManager.AUTO_TIMEZONE_POLICY,
+            new DefaultPolicyKey(DevicePolicyManager.AUTO_TIMEZONE_POLICY),
             // auto timezone is enabled by default, hence disabling it is more restrictive.
             FALSE_MORE_RESTRICTIVE,
             POLICY_FLAG_GLOBAL_ONLY_POLICY,
-            (Boolean value, Context context, Integer userId, Bundle args) ->
+            (Boolean value, Context context, Integer userId, PolicyKey policyKey) ->
                     PolicyEnforcerCallbacks.setAutoTimezoneEnabled(value, context),
             new BooleanPolicySerializer());
 
     // This is saved in the static map sPolicyDefinitions so that we're able to reconstruct the
     // actual permission grant policy with the correct arguments (packageName and permission name)
     // when reading the policies from xml.
-    private static final PolicyDefinition<Integer> PERMISSION_GRANT_NO_ARGS =
-            new PolicyDefinition<>(DevicePolicyManager.PERMISSION_GRANT_POLICY_KEY,
+    static final PolicyDefinition<Integer> GENERIC_PERMISSION_GRANT =
+            new PolicyDefinition<>(
+                    new PermissionGrantStatePolicyKey(
+                            DevicePolicyManager.PERMISSION_GRANT_POLICY_KEY),
                     // TODO: is this really the best mechanism, what makes denied more
                     //  restrictive than
                     //  granted?
@@ -79,71 +77,125 @@
                     PolicyEnforcerCallbacks::setPermissionGrantState,
                     new IntegerPolicySerializer());
 
+    /**
+     * Passing in {@code null} for {@code packageName} or {@code permissionName} will return a
+     * {@link #GENERIC_PERMISSION_GRANT}.
+     */
     static PolicyDefinition<Integer> PERMISSION_GRANT(
-            @NonNull String packageName, @NonNull String permission) {
-        Bundle callbackArgs = new Bundle();
-        callbackArgs.putString(PolicyUpdatesReceiver.EXTRA_PACKAGE_NAME, packageName);
-        callbackArgs.putString(PolicyUpdatesReceiver.EXTRA_PERMISSION_NAME, permission);
-        return PERMISSION_GRANT_NO_ARGS.setArgs(
-                DevicePolicyManager.PERMISSION_GRANT_POLICY(packageName, permission), callbackArgs);
+            @NonNull String packageName, @NonNull String permissionName) {
+        if (packageName == null || permissionName == null) {
+            return GENERIC_PERMISSION_GRANT;
+        }
+        return GENERIC_PERMISSION_GRANT.createPolicyDefinition(
+                new PermissionGrantStatePolicyKey(
+                        DevicePolicyManager.PERMISSION_GRANT_POLICY_KEY,
+                        packageName,
+                        permissionName));
     }
 
     static PolicyDefinition<LockTaskPolicy> LOCK_TASK = new PolicyDefinition<>(
-            DevicePolicyManager.LOCK_TASK_POLICY,
+            new DefaultPolicyKey(DevicePolicyManager.LOCK_TASK_POLICY),
             new TopPriority<>(List.of(
                     // TODO(b/258166155): add correct device lock role name
                     EnforcingAdmin.getRoleAuthorityOf("DeviceLock"),
                     EnforcingAdmin.DPC_AUTHORITY)),
             POLICY_FLAG_LOCAL_ONLY_POLICY,
-            (LockTaskPolicy value, Context context, Integer userId, Bundle args) ->
+            (LockTaskPolicy value, Context context, Integer userId, PolicyKey policyKey) ->
                     PolicyEnforcerCallbacks.setLockTask(value, context, userId),
             new LockTaskPolicy.LockTaskPolicySerializer());
 
     static PolicyDefinition<Set<String>> USER_CONTROLLED_DISABLED_PACKAGES = new PolicyDefinition<>(
-            DevicePolicyManager.USER_CONTROL_DISABLED_PACKAGES,
+            new DefaultPolicyKey(DevicePolicyManager.USER_CONTROL_DISABLED_PACKAGES_POLICY),
             new SetUnion<>(),
-            (Set<String> value, Context context, Integer userId, Bundle args) ->
+            (Set<String> value, Context context, Integer userId, PolicyKey policyKey) ->
                     PolicyEnforcerCallbacks.setUserControlDisabledPackages(value, userId),
             new SetPolicySerializer<>());
 
+    // This is saved in the static map sPolicyDefinitions so that we're able to reconstruct the
+    // actual permission grant policy with the correct arguments (packageName and permission name)
+    // when reading the policies from xml.
+    static PolicyDefinition<ComponentName> GENERIC_PERSISTENT_PREFERRED_ACTIVITY =
+            new PolicyDefinition<>(
+                    new PersistentPreferredActivityPolicyKey(
+                            DevicePolicyManager.PERSISTENT_PREFERRED_ACTIVITY_POLICY),
+            new TopPriority<>(List.of(
+                    // TODO(b/258166155): add correct device lock role name
+                    EnforcingAdmin.getRoleAuthorityOf("DeviceLock"),
+                    EnforcingAdmin.DPC_AUTHORITY)),
+            POLICY_FLAG_LOCAL_ONLY_POLICY,
+            PolicyEnforcerCallbacks::addPersistentPreferredActivity,
+            new ComponentNamePolicySerializer());
+
+    /**
+     * Passing in {@code null} for {@code intentFilter} will return
+     * {@link #GENERIC_PERSISTENT_PREFERRED_ACTIVITY}.
+     */
+    static PolicyDefinition<ComponentName> PERSISTENT_PREFERRED_ACTIVITY(
+            IntentFilter intentFilter) {
+        if (intentFilter == null) {
+            return GENERIC_PERSISTENT_PREFERRED_ACTIVITY;
+        }
+        return GENERIC_PERSISTENT_PREFERRED_ACTIVITY.createPolicyDefinition(
+                new PersistentPreferredActivityPolicyKey(
+                        DevicePolicyManager.PERSISTENT_PREFERRED_ACTIVITY_POLICY, intentFilter));
+    }
+
+    // This is saved in the static map sPolicyDefinitions so that we're able to reconstruct the
+    // actual uninstall blocked policy with the correct arguments (i.e. packageName)
+    // when reading the policies from xml.
+    static PolicyDefinition<Boolean> GENERIC_PACKAGE_UNINSTALL_BLOCKED =
+            new PolicyDefinition<>(
+                    new PackageSpecificPolicyKey(
+                            DevicePolicyManager.PACKAGE_UNINSTALL_BLOCKED_POLICY),
+                    TRUE_MORE_RESTRICTIVE,
+                    POLICY_FLAG_LOCAL_ONLY_POLICY,
+                    PolicyEnforcerCallbacks::setUninstallBlocked,
+                    new BooleanPolicySerializer());
+
+    /**
+     * Passing in {@code null} for {@code packageName} will return
+     * {@link #GENERIC_PACKAGE_UNINSTALL_BLOCKED}.
+     */
+    static PolicyDefinition<Boolean> PACKAGE_UNINSTALL_BLOCKED(
+            String packageName) {
+        if (packageName == null) {
+            return GENERIC_PACKAGE_UNINSTALL_BLOCKED;
+        }
+        return GENERIC_PACKAGE_UNINSTALL_BLOCKED.createPolicyDefinition(
+                new PackageSpecificPolicyKey(
+                        DevicePolicyManager.PACKAGE_UNINSTALL_BLOCKED_POLICY, packageName));
+    }
+
     private static final Map<String, PolicyDefinition<?>> sPolicyDefinitions = Map.of(
             DevicePolicyManager.AUTO_TIMEZONE_POLICY, AUTO_TIMEZONE,
-            DevicePolicyManager.PERMISSION_GRANT_POLICY_KEY, PERMISSION_GRANT_NO_ARGS,
+            DevicePolicyManager.PERMISSION_GRANT_POLICY_KEY, GENERIC_PERMISSION_GRANT,
             DevicePolicyManager.LOCK_TASK_POLICY, LOCK_TASK,
-            DevicePolicyManager.USER_CONTROL_DISABLED_PACKAGES, USER_CONTROLLED_DISABLED_PACKAGES
+            DevicePolicyManager.USER_CONTROL_DISABLED_PACKAGES_POLICY,
+            USER_CONTROLLED_DISABLED_PACKAGES,
+            DevicePolicyManager.PERSISTENT_PREFERRED_ACTIVITY_POLICY,
+            GENERIC_PERSISTENT_PREFERRED_ACTIVITY,
+            DevicePolicyManager.PACKAGE_UNINSTALL_BLOCKED_POLICY, GENERIC_PACKAGE_UNINSTALL_BLOCKED
     );
 
 
-    private final String mPolicyKey;
-    private final String mPolicyDefinitionKey;
+    private final PolicyKey mPolicyKey;
     private final ResolutionMechanism<V> mResolutionMechanism;
     private final int mPolicyFlags;
     // A function that accepts  policy to apple, context, userId, callback arguments, and returns
     // true if the policy has been enforced successfully.
-    private final QuadFunction<V, Context, Integer, Bundle, Boolean> mPolicyEnforcerCallback;
-    private final Bundle mCallbackArgs;
+    private final QuadFunction<V, Context, Integer, PolicyKey, Boolean> mPolicyEnforcerCallback;
     private final PolicySerializer<V> mPolicySerializer;
 
-    private PolicyDefinition<V> setArgs(String key, Bundle callbackArgs) {
-        return new PolicyDefinition<>(key, mPolicyDefinitionKey, mResolutionMechanism,
-                mPolicyFlags, mPolicyEnforcerCallback, mPolicySerializer, callbackArgs);
+    private PolicyDefinition<V> createPolicyDefinition(PolicyKey key) {
+        return new PolicyDefinition<>(key, mResolutionMechanism, mPolicyFlags,
+                mPolicyEnforcerCallback, mPolicySerializer);
     }
 
     @NonNull
-    String getPolicyKey() {
+    PolicyKey getPolicyKey() {
         return mPolicyKey;
     }
 
-    @NonNull
-    String getPolicyDefinitionKey() {
-        return mPolicyDefinitionKey;
-    }
-
-    @Nullable
-    Bundle getCallbackArgs() {
-        return mCallbackArgs;
-    }
-
     /**
      * Returns {@code true} if the policy is a global policy by nature and can't be applied locally.
      */
@@ -164,7 +216,7 @@
     }
 
     boolean enforcePolicy(@Nullable V value, Context context, int userId) {
-        return mPolicyEnforcerCallback.apply(value, context, userId, mCallbackArgs);
+        return mPolicyEnforcerCallback.apply(value, context, userId, mPolicyKey);
     }
 
     /**
@@ -172,93 +224,54 @@
      * {@link Object#equals} implementation.
      */
     private PolicyDefinition(
-            String key,
+            PolicyKey key,
             ResolutionMechanism<V> resolutionMechanism,
-            QuadFunction<V, Context, Integer, Bundle, Boolean> policyEnforcerCallback,
+            QuadFunction<V, Context, Integer, PolicyKey, Boolean> policyEnforcerCallback,
             PolicySerializer<V> policySerializer) {
         this(key, resolutionMechanism, POLICY_FLAG_NONE, policyEnforcerCallback, policySerializer);
     }
 
     /**
-     * Callers must ensure that {@code policyType} have implemented an appropriate
-     * {@link Object#equals} implementation.
+     * Callers must ensure that custom {@code policyKeys} and {@code V} have an appropriate
+     * {@link Object#equals} and {@link Object#hashCode()} implementation.
      */
     private PolicyDefinition(
-            String key,
+            PolicyKey policyKey,
             ResolutionMechanism<V> resolutionMechanism,
             int policyFlags,
-            QuadFunction<V, Context, Integer, Bundle, Boolean> policyEnforcerCallback,
+            QuadFunction<V, Context, Integer, PolicyKey, Boolean> policyEnforcerCallback,
             PolicySerializer<V> policySerializer) {
-        this(key, key, resolutionMechanism, policyFlags, policyEnforcerCallback,
-                policySerializer, /* callbackArs= */ null);
-    }
-
-    /**
-     * Callers must ensure that {@code policyType} have implemented an appropriate
-     * {@link Object#equals} implementation.
-     */
-    private PolicyDefinition(
-            String policyKey,
-            String policyDefinitionKey,
-            ResolutionMechanism<V> resolutionMechanism,
-            int policyFlags,
-            QuadFunction<V, Context, Integer, Bundle, Boolean> policyEnforcerCallback,
-            PolicySerializer<V> policySerializer,
-            Bundle callbackArgs) {
         mPolicyKey = policyKey;
-        mPolicyDefinitionKey = policyDefinitionKey;
         mResolutionMechanism = resolutionMechanism;
         mPolicyFlags = policyFlags;
         mPolicyEnforcerCallback = policyEnforcerCallback;
         mPolicySerializer = policySerializer;
-        mCallbackArgs = callbackArgs;
 
         // TODO: maybe use this instead of manually adding to the map
 //        sPolicyDefinitions.put(policyDefinitionKey, this);
     }
 
     void saveToXml(TypedXmlSerializer serializer) throws IOException {
-        serializer.attribute(/* namespace= */ null, ATTR_POLICY_KEY, mPolicyKey);
-        serializer.attribute(
-                /* namespace= */ null, ATTR_POLICY_DEFINITION_KEY, mPolicyDefinitionKey);
-        serializer.attributeInt(
-                /* namespace= */ null, ATTR_CALLBACK_ARGS_SIZE,
-                mCallbackArgs == null ? 0 : mCallbackArgs.size());
-        if (mCallbackArgs != null) {
-            int i = 0;
-            for (String key : mCallbackArgs.keySet()) {
-                serializer.attribute(/* namespace= */ null,
-                        ATTR_CALLBACK_ARGS_KEY + i, key);
-                serializer.attribute(/* namespace= */ null,
-                        ATTR_CALLBACK_ARGS_VALUE + i, mCallbackArgs.getString(key));
-                i++;
-            }
-        }
+        // TODO: here and elsewhere, add tags to ensure attributes aren't overridden by duplication.
+        mPolicyKey.saveToXml(serializer);
     }
 
     static <V> PolicyDefinition<V> readFromXml(TypedXmlPullParser parser)
             throws XmlPullParserException, IOException {
-        String policyKey = parser.getAttributeValue(/* namespace= */ null, ATTR_POLICY_KEY);
-        String policyDefinitionKey = parser.getAttributeValue(
-                /* namespace= */ null, ATTR_POLICY_DEFINITION_KEY);
-        int size = parser.getAttributeInt(/* namespace= */ null, ATTR_CALLBACK_ARGS_SIZE);
-        Bundle callbackArgs = new Bundle();
-
-        for (int i = 0; i < size; i++) {
-            String key = parser.getAttributeValue(
-                    /* namespace= */ null, ATTR_CALLBACK_ARGS_KEY + i);
-            String value = parser.getAttributeValue(
-                    /* namespace= */ null, ATTR_CALLBACK_ARGS_VALUE + i);
-            callbackArgs.putString(key, value);
-        }
-
         // TODO: can we avoid casting?
-        if (callbackArgs.isEmpty()) {
-            return (PolicyDefinition<V>) sPolicyDefinitions.get(policyDefinitionKey);
-        } else {
-            return (PolicyDefinition<V>) sPolicyDefinitions.get(policyDefinitionKey).setArgs(
-                    policyKey, callbackArgs);
-        }
+        PolicyKey policyKey = readPolicyKeyFromXml(parser);
+        PolicyDefinition<V> genericPolicyDefinition =
+                (PolicyDefinition<V>) sPolicyDefinitions.get(policyKey.mKey);
+        return genericPolicyDefinition.createPolicyDefinition(policyKey);
+    }
+
+    static <V> PolicyKey readPolicyKeyFromXml(TypedXmlPullParser parser)
+            throws XmlPullParserException, IOException {
+        // TODO: can we avoid casting?
+        PolicyKey policyKey = DefaultPolicyKey.readGenericPolicyKeyFromXml(parser);
+        PolicyDefinition<V> genericPolicyDefinition =
+                (PolicyDefinition<V>) sPolicyDefinitions.get(policyKey.mKey);
+        return genericPolicyDefinition.mPolicyKey.readFromXml(parser);
     }
 
     void savePolicyValueToXml(TypedXmlSerializer serializer, String attributeName, V value)
@@ -273,8 +286,7 @@
 
     @Override
     public String toString() {
-        return "PolicyDefinition { mPolicyKey= " + mPolicyKey + ", mPolicyDefinitionKey= "
-                + mPolicyDefinitionKey + ", mResolutionMechanism= " + mResolutionMechanism
-                + ", mCallbackArgs= " + mCallbackArgs + ", mPolicyFlags= " + mPolicyFlags + " }";
+        return "PolicyDefinition{ mPolicyKey= " + mPolicyKey + ", mResolutionMechanism= "
+                + mResolutionMechanism + ", mPolicyFlags= " + mPolicyFlags + " }";
     }
 }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
index 5664d2b..e2aa23d 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
@@ -18,17 +18,21 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.AppGlobals;
 import android.app.admin.DevicePolicyManager;
-import android.app.admin.PolicyUpdatesReceiver;
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.IntentFilter;
+import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.os.Binder;
-import android.os.Bundle;
+import android.os.RemoteException;
 import android.os.UserHandle;
 import android.permission.AdminPermissionControlParams;
 import android.permission.PermissionControllerManager;
 import android.provider.Settings;
+import android.util.Slog;
 
 import com.android.server.LocalServices;
 import com.android.server.utils.Slogf;
@@ -58,19 +62,15 @@
 
     static boolean setPermissionGrantState(
             @Nullable Integer grantState, @NonNull Context context, int userId,
-            @NonNull Bundle args) {
+            @NonNull PolicyKey policyKey) {
         return Boolean.TRUE.equals(Binder.withCleanCallingIdentity(() -> {
-            if (args == null
-                    || !args.containsKey(PolicyUpdatesReceiver.EXTRA_PACKAGE_NAME)
-                    || !args.containsKey(PolicyUpdatesReceiver.EXTRA_PERMISSION_NAME)) {
-                throw new IllegalArgumentException("Package name and permission name must be "
-                        + "provided as arguments.");
+            if (!(policyKey instanceof PermissionGrantStatePolicyKey)) {
+                throw new IllegalArgumentException("policyKey is not of type "
+                        + "PermissionGrantStatePolicyKey");
             }
-
-            String packageName = args.getString(PolicyUpdatesReceiver.EXTRA_PACKAGE_NAME);
-            String permissionName = args.getString(PolicyUpdatesReceiver.EXTRA_PERMISSION_NAME);
-            Objects.requireNonNull(packageName);
-            Objects.requireNonNull(permissionName);
+            PermissionGrantStatePolicyKey parsedKey = (PermissionGrantStatePolicyKey) policyKey;
+            Objects.requireNonNull(parsedKey.getPermissionName());
+            Objects.requireNonNull(parsedKey.getPackageName());
             Objects.requireNonNull(context);
 
             int value = grantState == null
@@ -81,7 +81,7 @@
             // TODO: remove canAdminGrantSensorPermissions once we expose a new method in
             //  permissionController that doesn't need it.
             AdminPermissionControlParams permissionParams = new AdminPermissionControlParams(
-                    packageName, permissionName, value,
+                    parsedKey.getPackageName(), parsedKey.getPermissionName(), value,
                     /* canAdminGrantSensorPermissions= */ true);
             getPermissionControllerManager(context, UserHandle.of(userId))
                     // TODO: remove callingPackage param and stop passing context.getPackageName()
@@ -150,4 +150,51 @@
                                 packages == null ? null : packages.stream().toList()));
         return true;
     }
+
+    static boolean addPersistentPreferredActivity(
+            @Nullable ComponentName preferredActivity, @NonNull Context context, int userId,
+            @NonNull PolicyKey policyKey) {
+        Binder.withCleanCallingIdentity(() -> {
+            try {
+                if (!(policyKey instanceof PersistentPreferredActivityPolicyKey)) {
+                    throw new IllegalArgumentException("policyKey is not of type "
+                            + "PersistentPreferredActivityPolicyKey");
+                }
+                PersistentPreferredActivityPolicyKey parsedKey =
+                        (PersistentPreferredActivityPolicyKey) policyKey;
+                IntentFilter filter = Objects.requireNonNull(parsedKey.getFilter());
+
+                IPackageManager packageManager = AppGlobals.getPackageManager();
+                if (preferredActivity != null) {
+                    packageManager.addPersistentPreferredActivity(
+                            filter, preferredActivity, userId);
+                } else {
+                    packageManager.clearPersistentPreferredActivity(filter, userId);
+                }
+                packageManager.flushPackageRestrictionsAsUser(userId);
+            } catch (RemoteException re) {
+                // Shouldn't happen
+                Slog.wtf(LOG_TAG, "Error adding/removing persistent preferred activity", re);
+            }
+        });
+        return true;
+    }
+
+    static boolean setUninstallBlocked(
+            @Nullable Boolean uninstallBlocked, @NonNull Context context, int userId,
+            @NonNull PolicyKey policyKey) {
+        return Boolean.TRUE.equals(Binder.withCleanCallingIdentity(() -> {
+            if (!(policyKey instanceof PackageSpecificPolicyKey)) {
+                throw new IllegalArgumentException("policyKey is not of type "
+                        + "PackageSpecificPolicyKey");
+            }
+            PackageSpecificPolicyKey parsedKey = (PackageSpecificPolicyKey) policyKey;
+            String packageName = Objects.requireNonNull(parsedKey.getPackageName());
+            DevicePolicyManagerService.setUninstallBlockedUnchecked(
+                    packageName,
+                    uninstallBlocked != null && uninstallBlocked,
+                    userId);
+            return true;
+        }));
+    }
 }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyKey.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyKey.java
new file mode 100644
index 0000000..571f0ee
--- /dev/null
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyKey.java
@@ -0,0 +1,81 @@
+/*
+ * 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.server.devicepolicy;
+
+import static android.app.admin.PolicyUpdatesReceiver.EXTRA_POLICY_KEY;
+
+import android.annotation.Nullable;
+import android.os.Bundle;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * Abstract class used to identify a policy in the policy engine's data structure.
+ */
+abstract class PolicyKey {
+    private static final String ATTR_GENERIC_POLICY_KEY = "generic-policy-key";
+
+    protected final String mKey;
+
+    PolicyKey(String policyKey) {
+        mKey = Objects.requireNonNull(policyKey);
+    }
+
+    String getKey() {
+        return mKey;
+    }
+
+    boolean hasSameKeyAs(PolicyKey other) {
+        if (other == null) {
+            return false;
+        }
+        return mKey.equals(other.mKey);
+    }
+
+    void saveToXml(TypedXmlSerializer serializer) throws IOException {
+        serializer.attribute(/* namespace= */ null, ATTR_GENERIC_POLICY_KEY, mKey);
+    }
+
+    PolicyKey readFromXml(TypedXmlPullParser parser)
+            throws XmlPullParserException, IOException {
+        // No need to read anything
+        return this;
+    }
+
+    void writeToBundle(Bundle bundle) {
+        bundle.putString(EXTRA_POLICY_KEY, mKey);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        PolicyKey other = (PolicyKey) o;
+        return Objects.equals(mKey, other.mKey);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mKey);
+    }
+}
diff --git a/services/robotests/backup/src/com/android/server/backup/FullBackupJobTest.java b/services/robotests/backup/src/com/android/server/backup/FullBackupJobTest.java
index dbc0da7..c8797e2 100644
--- a/services/robotests/backup/src/com/android/server/backup/FullBackupJobTest.java
+++ b/services/robotests/backup/src/com/android/server/backup/FullBackupJobTest.java
@@ -18,6 +18,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.Mockito.when;
+
 import android.annotation.UserIdInt;
 import android.app.job.JobScheduler;
 import android.content.Context;
@@ -31,6 +33,8 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.Shadows;
@@ -45,14 +49,20 @@
     private BackupManagerConstants mConstants;
     private ShadowJobScheduler mShadowJobScheduler;
 
+    @Mock
+    private UserBackupManagerService mUserBackupManagerService;
+
     @UserIdInt private int mUserOneId;
     @UserIdInt private int mUserTwoId;
 
     @Before
     public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
         mContext = RuntimeEnvironment.application;
         mConstants = new BackupManagerConstants(Handler.getMain(), mContext.getContentResolver());
         mConstants.start();
+        when(mUserBackupManagerService.getConstants()).thenReturn(mConstants);
+        when(mUserBackupManagerService.isFrameworkSchedulingEnabled()).thenReturn(true);
 
         mShadowJobScheduler = Shadows.shadowOf(mContext.getSystemService(JobScheduler.class));
 
@@ -69,8 +79,8 @@
 
     @Test
     public void testSchedule_afterScheduling_jobExists() {
-        FullBackupJob.schedule(mUserOneId, mContext, 0, mConstants);
-        FullBackupJob.schedule(mUserTwoId, mContext, 0, mConstants);
+        FullBackupJob.schedule(mUserOneId, mContext, 0, mUserBackupManagerService);
+        FullBackupJob.schedule(mUserTwoId, mContext, 0, mUserBackupManagerService);
 
         assertThat(mShadowJobScheduler.getPendingJob(getJobIdForUserId(mUserOneId))).isNotNull();
         assertThat(mShadowJobScheduler.getPendingJob(getJobIdForUserId(mUserTwoId))).isNotNull();
@@ -78,18 +88,34 @@
 
     @Test
     public void testCancel_afterCancelling_jobDoesntExist() {
-        FullBackupJob.schedule(mUserOneId, mContext, 0, mConstants);
-        FullBackupJob.schedule(mUserTwoId, mContext, 0, mConstants);
+        FullBackupJob.schedule(mUserOneId, mContext, 0, mUserBackupManagerService);
+        FullBackupJob.schedule(mUserTwoId, mContext, 0, mUserBackupManagerService);
         FullBackupJob.cancel(mUserOneId, mContext);
         FullBackupJob.cancel(mUserTwoId, mContext);
 
         assertThat(mShadowJobScheduler.getPendingJob(getJobIdForUserId(mUserOneId))).isNull();
         assertThat(mShadowJobScheduler.getPendingJob(getJobIdForUserId(mUserTwoId))).isNull();
     }
+
+    @Test
+    public void testSchedule_isNoopIfDisabled() {
+        when(mUserBackupManagerService.isFrameworkSchedulingEnabled()).thenReturn(false);
+        FullBackupJob.schedule(mUserOneId, mContext, 0, mUserBackupManagerService);
+
+        assertThat(mShadowJobScheduler.getPendingJob(getJobIdForUserId(mUserOneId))).isNull();
+    }
+
+    @Test
+    public void testSchedule_schedulesJobIfEnabled() {
+        when(mUserBackupManagerService.isFrameworkSchedulingEnabled()).thenReturn(true);
+        FullBackupJob.schedule(mUserOneId, mContext, 0, mUserBackupManagerService);
+
+        assertThat(mShadowJobScheduler.getPendingJob(getJobIdForUserId(mUserOneId))).isNotNull();
+    }
 //
     @Test
     public void testSchedule_onlySchedulesForRequestedUser() {
-        FullBackupJob.schedule(mUserOneId, mContext, 0, mConstants);
+        FullBackupJob.schedule(mUserOneId, mContext, 0, mUserBackupManagerService);
 
         assertThat(mShadowJobScheduler.getPendingJob(getJobIdForUserId(mUserOneId))).isNotNull();
         assertThat(mShadowJobScheduler.getPendingJob(getJobIdForUserId(mUserTwoId))).isNull();
@@ -97,8 +123,8 @@
 //
     @Test
     public void testCancel_onlyCancelsForRequestedUser() {
-        FullBackupJob.schedule(mUserOneId, mContext, 0, mConstants);
-        FullBackupJob.schedule(mUserTwoId, mContext, 0, mConstants);
+        FullBackupJob.schedule(mUserOneId, mContext, 0, mUserBackupManagerService);
+        FullBackupJob.schedule(mUserTwoId, mContext, 0, mUserBackupManagerService);
         FullBackupJob.cancel(mUserOneId, mContext);
 
         assertThat(mShadowJobScheduler.getPendingJob(getJobIdForUserId(mUserOneId))).isNull();
diff --git a/services/robotests/backup/src/com/android/server/backup/KeyValueBackupJobTest.java b/services/robotests/backup/src/com/android/server/backup/KeyValueBackupJobTest.java
index 1c5fac2..712ac55 100644
--- a/services/robotests/backup/src/com/android/server/backup/KeyValueBackupJobTest.java
+++ b/services/robotests/backup/src/com/android/server/backup/KeyValueBackupJobTest.java
@@ -18,6 +18,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.Mockito.when;
+
 import android.annotation.UserIdInt;
 import android.content.Context;
 import android.os.Handler;
@@ -30,6 +32,8 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Config;
@@ -41,14 +45,20 @@
     private Context mContext;
     private BackupManagerConstants mConstants;
 
+    @Mock
+    private UserBackupManagerService mUserBackupManagerService;
+
     @UserIdInt private int mUserOneId;
     @UserIdInt private int mUserTwoId;
 
     @Before
     public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
         mContext = RuntimeEnvironment.application;
         mConstants = new BackupManagerConstants(Handler.getMain(), mContext.getContentResolver());
         mConstants.start();
+        when(mUserBackupManagerService.getConstants()).thenReturn(mConstants);
+        when(mUserBackupManagerService.isFrameworkSchedulingEnabled()).thenReturn(true);
 
         mUserOneId = UserHandle.USER_SYSTEM;
         mUserTwoId = mUserOneId + 1;
@@ -62,6 +72,22 @@
     }
 
     @Test
+    public void testSchedule_isNoopIfDisabled() {
+        when(mUserBackupManagerService.isFrameworkSchedulingEnabled()).thenReturn(false);
+        KeyValueBackupJob.schedule(mUserOneId, mContext, mUserBackupManagerService);
+
+        assertThat(KeyValueBackupJob.isScheduled(mUserOneId)).isFalse();
+    }
+
+    @Test
+    public void testSchedule_schedulesJobIfEnabled() {
+        when(mUserBackupManagerService.isFrameworkSchedulingEnabled()).thenReturn(true);
+        KeyValueBackupJob.schedule(mUserOneId, mContext, mUserBackupManagerService);
+
+        assertThat(KeyValueBackupJob.isScheduled(mUserOneId)).isTrue();
+    }
+
+    @Test
     public void testIsScheduled_beforeScheduling_returnsFalse() {
         assertThat(KeyValueBackupJob.isScheduled(mUserOneId)).isFalse();
         assertThat(KeyValueBackupJob.isScheduled(mUserTwoId)).isFalse();
@@ -69,8 +95,8 @@
 
     @Test
     public void testIsScheduled_afterScheduling_returnsTrue() {
-        KeyValueBackupJob.schedule(mUserOneId, mContext, mConstants);
-        KeyValueBackupJob.schedule(mUserTwoId, mContext, mConstants);
+        KeyValueBackupJob.schedule(mUserOneId, mContext, mUserBackupManagerService);
+        KeyValueBackupJob.schedule(mUserTwoId, mContext, mUserBackupManagerService);
 
         assertThat(KeyValueBackupJob.isScheduled(mUserOneId)).isTrue();
         assertThat(KeyValueBackupJob.isScheduled(mUserTwoId)).isTrue();
@@ -78,8 +104,8 @@
 
     @Test
     public void testIsScheduled_afterCancelling_returnsFalse() {
-        KeyValueBackupJob.schedule(mUserOneId, mContext, mConstants);
-        KeyValueBackupJob.schedule(mUserTwoId, mContext, mConstants);
+        KeyValueBackupJob.schedule(mUserOneId, mContext, mUserBackupManagerService);
+        KeyValueBackupJob.schedule(mUserTwoId, mContext, mUserBackupManagerService);
         KeyValueBackupJob.cancel(mUserOneId, mContext);
         KeyValueBackupJob.cancel(mUserTwoId, mContext);
 
@@ -89,7 +115,7 @@
 
     @Test
     public void testIsScheduled_afterScheduling_returnsTrueOnlyForScheduledUser() {
-        KeyValueBackupJob.schedule(mUserOneId, mContext, mConstants);
+        KeyValueBackupJob.schedule(mUserOneId, mContext, mUserBackupManagerService);
 
         assertThat(KeyValueBackupJob.isScheduled(mUserOneId)).isTrue();
         assertThat(KeyValueBackupJob.isScheduled(mUserTwoId)).isFalse();
@@ -97,8 +123,8 @@
 
     @Test
     public void testIsScheduled_afterCancelling_returnsFalseOnlyForCancelledUser() {
-        KeyValueBackupJob.schedule(mUserOneId, mContext, mConstants);
-        KeyValueBackupJob.schedule(mUserTwoId, mContext, mConstants);
+        KeyValueBackupJob.schedule(mUserOneId, mContext, mUserBackupManagerService);
+        KeyValueBackupJob.schedule(mUserTwoId, mContext, mUserBackupManagerService);
         KeyValueBackupJob.cancel(mUserOneId, mContext);
 
         assertThat(KeyValueBackupJob.isScheduled(mUserOneId)).isFalse();
diff --git a/services/robotests/backup/src/com/android/server/backup/UserBackupManagerServiceTest.java b/services/robotests/backup/src/com/android/server/backup/UserBackupManagerServiceTest.java
index 2878743..02e0bbf 100644
--- a/services/robotests/backup/src/com/android/server/backup/UserBackupManagerServiceTest.java
+++ b/services/robotests/backup/src/com/android/server/backup/UserBackupManagerServiceTest.java
@@ -1381,8 +1381,8 @@
          * BackupManagerConstants)} that throws an {@link IllegalArgumentException}.
          */
         public static void schedule(int userId, Context ctx, long delay,
-                BackupManagerConstants constants) {
-            ShadowKeyValueBackupJob.schedule(userId, ctx, delay, constants);
+                UserBackupManagerService userBackupManagerService) {
+            ShadowKeyValueBackupJob.schedule(userId, ctx, delay, userBackupManagerService);
             throw new IllegalArgumentException();
         }
     }
diff --git a/services/robotests/src/com/android/server/testing/shadows/ShadowKeyValueBackupJob.java b/services/robotests/src/com/android/server/testing/shadows/ShadowKeyValueBackupJob.java
index f90ea6a..d66f6ef 100644
--- a/services/robotests/src/com/android/server/testing/shadows/ShadowKeyValueBackupJob.java
+++ b/services/robotests/src/com/android/server/testing/shadows/ShadowKeyValueBackupJob.java
@@ -21,6 +21,7 @@
 
 import com.android.server.backup.BackupManagerConstants;
 import com.android.server.backup.KeyValueBackupJob;
+import com.android.server.backup.UserBackupManagerService;
 
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
@@ -35,7 +36,7 @@
 
     @Implementation
     protected static void schedule(int userId, Context ctx, long delay,
-            BackupManagerConstants constants) {
+            UserBackupManagerService userBackupManagerService) {
         callingUid = Binder.getCallingUid();
     }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
index f13de12..d03d196 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
@@ -34,6 +34,7 @@
 import android.os.storage.StorageManager;
 import android.provider.Settings;
 import android.util.Log;
+import android.util.Pair;
 import android.util.SparseArray;
 
 import androidx.test.annotation.UiThreadTest;
@@ -154,6 +155,15 @@
     }
 
     @Test
+    public void testGetCurrentAndTargetUserIds() {
+        mockCurrentAndTargetUser(USER_ID, OTHER_USER_ID);
+
+        assertWithMessage("getCurrentAndTargetUserIds()")
+                .that(mUms.getCurrentAndTargetUserIds())
+                .isEqualTo(new Pair<>(USER_ID, OTHER_USER_ID));
+    }
+
+    @Test
     public void testGetCurrentUserId() {
         mockCurrentUser(USER_ID);
 
@@ -329,6 +339,14 @@
         when(mActivityManagerInternal.getCurrentUserId()).thenReturn(userId);
     }
 
+    private void mockCurrentAndTargetUser(@UserIdInt int currentUserId,
+            @UserIdInt int targetUserId) {
+        mockGetLocalService(ActivityManagerInternal.class, mActivityManagerInternal);
+
+        when(mActivityManagerInternal.getCurrentAndTargetUserIds())
+                .thenReturn(new Pair<>(currentUserId, targetUserId));
+    }
+
     private <T> void mockGetLocalService(Class<T> serviceClass, T service) {
         doReturn(service).when(() -> LocalServices.getService(serviceClass));
     }
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserLifecycleStressTest.java b/services/tests/servicestests/src/com/android/server/pm/UserLifecycleStressTest.java
index bbe8907..c9f00d7 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserLifecycleStressTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserLifecycleStressTest.java
@@ -23,12 +23,12 @@
 
 import android.app.ActivityManager;
 import android.app.IStopUserCallback;
-import android.app.UserSwitchObserver;
 import android.content.Context;
 import android.content.pm.UserInfo;
 import android.os.RemoteException;
 import android.os.UserManager;
 import android.platform.test.annotations.Postsubmit;
+import android.provider.Settings;
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
@@ -37,10 +37,12 @@
 
 import com.android.internal.util.FunctionalUtils;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.IOException;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -56,18 +58,30 @@
     private static final String TAG = "UserLifecycleStressTest";
     // TODO: Make this smaller once we have improved it.
     private static final int TIMEOUT_IN_SECOND = 40;
-    private static final int NUM_ITERATIONS = 10;
+    private static final int NUM_ITERATIONS = 8;
     private static final int WAIT_BEFORE_STOP_USER_IN_SECOND = 3;
 
     private Context mContext;
     private UserManager mUserManager;
     private ActivityManager mActivityManager;
+    private UserSwitchWaiter mUserSwitchWaiter;
+    private String mRemoveGuestOnExitOriginalValue;
 
     @Before
-    public void setup() {
+    public void setup() throws RemoteException {
         mContext = InstrumentationRegistry.getInstrumentation().getContext();
         mUserManager = mContext.getSystemService(UserManager.class);
         mActivityManager = mContext.getSystemService(ActivityManager.class);
+        mUserSwitchWaiter = new UserSwitchWaiter(TAG, TIMEOUT_IN_SECOND);
+        mRemoveGuestOnExitOriginalValue = Settings.Global.getString(mContext.getContentResolver(),
+                Settings.Global.REMOVE_GUEST_ON_EXIT);
+    }
+
+    @After
+    public void tearDown() throws IOException {
+        mUserSwitchWaiter.close();
+        Settings.Global.putString(mContext.getContentResolver(),
+                Settings.Global.REMOVE_GUEST_ON_EXIT, mRemoveGuestOnExitOriginalValue);
     }
 
     /**
@@ -101,10 +115,13 @@
      * 1. While the guest user is in foreground, mark it for deletion.
      * 2. Create a new guest. (This wouldn't be possible if the old one wasn't marked for deletion)
      * 3. Switch to newly created guest.
-     * 4. Remove the previous guest before waiting for switch to complete.
+     * 4. Remove the previous guest after the switch is complete.
      **/
     @Test
-    public void switchToExistingGuestAndStartOverStressTest() throws Exception {
+    public void switchToExistingGuestAndStartOverStressTest() {
+        Settings.Global.putString(mContext.getContentResolver(),
+                Settings.Global.REMOVE_GUEST_ON_EXIT, "0");
+
         if (ActivityManager.getCurrentUser() != USER_SYSTEM) {
             switchUser(USER_SYSTEM);
         }
@@ -135,14 +152,15 @@
                     .isNotNull();
 
             Log.d(TAG, "Switching to the new guest");
-            switchUserThenRun(newGuest.id, () -> {
-                if (currentGuestId != USER_NULL) {
-                    Log.d(TAG, "Removing the previous guest before waiting for switch to complete");
-                    assertWithMessage("Couldn't remove guest")
-                            .that(mUserManager.removeUser(currentGuestId))
-                            .isTrue();
-                }
-            });
+            switchUser(newGuest.id);
+
+            if (currentGuestId != USER_NULL) {
+                Log.d(TAG, "Removing the previous guest");
+                assertWithMessage("Couldn't remove guest")
+                        .that(mUserManager.removeUser(currentGuestId))
+                        .isTrue();
+            }
+
             Log.d(TAG, "Switching back to the system user");
             switchUser(USER_SYSTEM);
 
@@ -174,33 +192,14 @@
     }
 
     /** Starts the given user in the foreground and waits for the switch to finish. */
-    private void switchUser(int userId) throws RemoteException, InterruptedException {
-        switchUserThenRun(userId, null);
-    }
+    private void switchUser(int userId) {
+        Log.d(TAG, "Switching to user " + userId);
 
-    /**
-     * Starts the given user in the foreground. And runs the given Runnable right after
-     * am.switchUser call, before waiting for the actual user switch to be complete.
-     **/
-    private void switchUserThenRun(int userId, Runnable runAfterSwitchBeforeWait)
-            throws RemoteException, InterruptedException {
-        runWithLatch("switch user", countDownLatch -> {
-            ActivityManager.getService().registerUserSwitchObserver(
-                    new UserSwitchObserver() {
-                        @Override
-                        public void onUserSwitchComplete(int newUserId) {
-                            if (userId == newUserId) {
-                                countDownLatch.countDown();
-                            }
-                        }
-                    }, TAG);
-            Log.d(TAG, "Switching to user " + userId);
-            assertWithMessage("Failed to switch to user")
-                    .that(mActivityManager.switchUser(userId))
-                    .isTrue();
-            if (runAfterSwitchBeforeWait != null) {
-                runAfterSwitchBeforeWait.run();
-            }
+        mUserSwitchWaiter.runThenWaitUntilSwitchCompleted(userId, () -> {
+            assertWithMessage("Could not start switching to user " + userId)
+                    .that(mActivityManager.switchUser(userId)).isTrue();
+        }, /* onFail= */ () -> {
+            throw new AssertionError("Could not complete switching to user " + userId);
         });
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
index 1889d9a..76a13f1 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -75,7 +75,7 @@
 
     private static final int REMOVE_CHECK_INTERVAL_MILLIS = 500; // 0.5 seconds
     private static final int REMOVE_TIMEOUT_MILLIS = 60 * 1000; // 60 seconds
-    private static final int SWITCH_USER_TIMEOUT_MILLIS = 40 * 1000; // 40 seconds
+    private static final int SWITCH_USER_TIMEOUT_SECONDS = 40; // 40 seconds
 
     // Packages which are used during tests.
     private static final String[] PACKAGES = new String[] {
@@ -87,19 +87,21 @@
     private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
 
     private final Object mUserRemoveLock = new Object();
-    private final Object mUserSwitchLock = new Object();
 
     private UserManager mUserManager = null;
+    private ActivityManager mActivityManager;
     private PackageManager mPackageManager;
     private List<Integer> usersToRemove;
+    private UserSwitchWaiter mUserSwitchWaiter;
 
     @Before
     public void setUp() throws Exception {
         mUserManager = UserManager.get(mContext);
+        mActivityManager = mContext.getSystemService(ActivityManager.class);
         mPackageManager = mContext.getPackageManager();
+        mUserSwitchWaiter = new UserSwitchWaiter(TAG, SWITCH_USER_TIMEOUT_SECONDS);
 
         IntentFilter filter = new IntentFilter(Intent.ACTION_USER_REMOVED);
-        filter.addAction(Intent.ACTION_USER_SWITCHED);
         mContext.registerReceiver(new BroadcastReceiver() {
             @Override
             public void onReceive(Context context, Intent intent) {
@@ -109,11 +111,6 @@
                             mUserRemoveLock.notifyAll();
                         }
                         break;
-                    case Intent.ACTION_USER_SWITCHED:
-                        synchronized (mUserSwitchLock) {
-                            mUserSwitchLock.notifyAll();
-                        }
-                        break;
                 }
             }
         }, filter);
@@ -124,6 +121,7 @@
 
     @After
     public void tearDown() throws Exception {
+        mUserSwitchWaiter.close();
         for (Integer userId : usersToRemove) {
             removeUser(userId);
         }
@@ -322,6 +320,66 @@
 
     @MediumTest
     @Test
+    public void testRemoveUserShouldNotRemoveCurrentUser() {
+        final int startUser = ActivityManager.getCurrentUser();
+        final UserInfo testUser = createUser("TestUser", /* flags= */ 0);
+        // Switch to the user just created.
+        switchUser(testUser.id);
+
+        assertWithMessage("Current user should not be removed")
+                .that(mUserManager.removeUser(testUser.id))
+                .isFalse();
+
+        // Switch back to the starting user.
+        switchUser(startUser);
+
+        // Now we can remove the user
+        removeUser(testUser.id);
+    }
+
+    @MediumTest
+    @Test
+    public void testRemoveUserShouldNotRemoveCurrentUser_DuringUserSwitch() {
+        final int startUser = ActivityManager.getCurrentUser();
+        final UserInfo testUser = createUser("TestUser", /* flags= */ 0);
+        // Switch to the user just created.
+        switchUser(testUser.id);
+
+        switchUserThenRun(startUser, () -> {
+            // While the user switch is happening, call removeUser for the current user.
+            assertWithMessage("Current user should not be removed during user switch")
+                    .that(mUserManager.removeUser(testUser.id))
+                    .isFalse();
+        });
+        assertThat(hasUser(testUser.id)).isTrue();
+
+        // Now we can remove the user
+        removeUser(testUser.id);
+    }
+
+    @MediumTest
+    @Test
+    public void testRemoveUserShouldNotRemoveTargetUser_DuringUserSwitch() {
+        final int startUser = ActivityManager.getCurrentUser();
+        final UserInfo testUser = createUser("TestUser", /* flags= */ 0);
+
+        switchUserThenRun(testUser.id, () -> {
+            // While the user switch is happening, call removeUser for the target user.
+            assertWithMessage("Target user should not be removed during user switch")
+                    .that(mUserManager.removeUser(testUser.id))
+                    .isFalse();
+        });
+        assertThat(hasUser(testUser.id)).isTrue();
+
+        // Switch back to the starting user.
+        switchUser(startUser);
+
+        // Now we can remove the user
+        removeUser(testUser.id);
+    }
+
+    @MediumTest
+    @Test
     public void testRemoveUserWhenPossible_restrictedReturnsError() throws Exception {
         final int currentUser = ActivityManager.getCurrentUser();
         final UserInfo user1 = createUser("User 1", /* flags= */ 0);
@@ -383,7 +441,7 @@
         final UserInfo otherUser = createUser("User 1", /* flags= */ UserInfo.FLAG_ADMIN);
         UserHandle mainUser = mUserManager.getMainUser();
 
-        switchUser(otherUser.id, null, true);
+        switchUser(otherUser.id);
 
         assertThat(mUserManager.removeUserWhenPossible(mainUser,
                 /* overrideDevicePolicy= */ false))
@@ -393,7 +451,7 @@
         assertThat(hasUser(mainUser.getIdentifier())).isTrue();
 
         // Switch back to the starting user.
-        switchUser(currentUser, null, true);
+        switchUser(currentUser);
     }
 
     @MediumTest
@@ -411,7 +469,7 @@
         final int startUser = ActivityManager.getCurrentUser();
         final UserInfo user1 = createUser("User 1", /* flags= */ 0);
         // Switch to the user just created.
-        switchUser(user1.id, null, /* ignoreHandle= */ true);
+        switchUser(user1.id);
 
         assertThat(mUserManager.removeUserWhenPossible(user1.getUserHandle(),
                 /* overrideDevicePolicy= */ false)).isEqualTo(UserManager.REMOVE_RESULT_DEFERRED);
@@ -420,7 +478,7 @@
         assertThat(getUser(user1.id).isEphemeral()).isTrue();
 
         // Switch back to the starting user.
-        switchUser(startUser, null, /* ignoreHandle= */ true);
+        switchUser(startUser);
 
         // User is removed once switch is complete
         synchronized (mUserRemoveLock) {
@@ -431,6 +489,55 @@
 
     @MediumTest
     @Test
+    public void testRemoveUserWhenPossible_currentUserSetEphemeral_duringUserSwitch() {
+        final int startUser = ActivityManager.getCurrentUser();
+        final UserInfo testUser = createUser("TestUser", /* flags= */ 0);
+        // Switch to the user just created.
+        switchUser(testUser.id);
+
+        switchUserThenRun(startUser, () -> {
+            // While the user switch is happening, call removeUserWhenPossible for the current user.
+            assertThat(mUserManager.removeUserWhenPossible(testUser.getUserHandle(), false))
+                    .isEqualTo(UserManager.REMOVE_RESULT_DEFERRED);
+
+            assertThat(hasUser(testUser.id)).isTrue();
+            assertThat(getUser(testUser.id).isEphemeral()).isTrue();
+        });
+
+        // User is removed once switch is complete
+        synchronized (mUserRemoveLock) {
+            waitForUserRemovalLocked(testUser.id);
+        }
+        assertThat(hasUser(testUser.id)).isFalse();
+    }
+
+    @MediumTest
+    @Test
+    public void testRemoveUserWhenPossible_targetUserSetEphemeral_duringUserSwitch() {
+        final int startUser = ActivityManager.getCurrentUser();
+        final UserInfo testUser = createUser("TestUser", /* flags= */ 0);
+
+        switchUserThenRun(testUser.id, () -> {
+            // While the user switch is happening, call removeUserWhenPossible for the target user.
+            assertThat(mUserManager.removeUserWhenPossible(testUser.getUserHandle(), false))
+                    .isEqualTo(UserManager.REMOVE_RESULT_DEFERRED);
+
+            assertThat(hasUser(testUser.id)).isTrue();
+            assertThat(getUser(testUser.id).isEphemeral()).isTrue();
+        });
+
+        // Switch back to the starting user.
+        switchUser(startUser);
+
+        // User is removed once switch is complete
+        synchronized (mUserRemoveLock) {
+            waitForUserRemovalLocked(testUser.id);
+        }
+        assertThat(hasUser(testUser.id)).isFalse();
+    }
+
+    @MediumTest
+    @Test
     public void testRemoveUserWhenPossible_nonCurrentUserRemoved() throws Exception {
         final UserInfo user1 = createUser("User 1", /* flags= */ 0);
         synchronized (mUserRemoveLock) {
@@ -1149,33 +1256,30 @@
     @LargeTest
     @Test
     public void testSwitchUser() {
-        ActivityManager am = mContext.getSystemService(ActivityManager.class);
-        final int startUser = am.getCurrentUser();
+        final int startUser = ActivityManager.getCurrentUser();
         UserInfo user = createUser("User", 0);
         assertThat(user).isNotNull();
         // Switch to the user just created.
-        switchUser(user.id, null, true);
+        switchUser(user.id);
         // Switch back to the starting user.
-        switchUser(startUser, null, true);
+        switchUser(startUser);
     }
 
     @LargeTest
     @Test
     public void testSwitchUserByHandle() {
-        ActivityManager am = mContext.getSystemService(ActivityManager.class);
-        final int startUser = am.getCurrentUser();
+        final int startUser = ActivityManager.getCurrentUser();
         UserInfo user = createUser("User", 0);
         assertThat(user).isNotNull();
         // Switch to the user just created.
-        switchUser(-1, user.getUserHandle(), false);
+        switchUser(user.getUserHandle());
         // Switch back to the starting user.
-        switchUser(-1, UserHandle.of(startUser), false);
+        switchUser(UserHandle.of(startUser));
     }
 
     @Test
     public void testSwitchUserByHandle_ThrowsException() {
-        ActivityManager am = mContext.getSystemService(ActivityManager.class);
-        assertThrows(IllegalArgumentException.class, () -> am.switchUser(null));
+        assertThrows(IllegalArgumentException.class, () -> mActivityManager.switchUser(null));
     }
 
     @MediumTest
@@ -1319,31 +1423,43 @@
     }
 
     /**
-     * @param userId value will be used to call switchUser(int) only if ignoreHandle is false.
-     * @param user value will be used to call switchUser(UserHandle) only if ignoreHandle is true.
-     * @param ignoreHandle if true, switchUser(int) will be called with the provided userId,
-     *                     else, switchUser(UserHandle) will be called with the provided user.
-     */
-    private void switchUser(int userId, UserHandle user, boolean ignoreHandle) {
-        synchronized (mUserSwitchLock) {
-            ActivityManager am = mContext.getSystemService(ActivityManager.class);
-            if (ignoreHandle) {
-                am.switchUser(userId);
-            } else {
-                am.switchUser(user);
+     * Starts the given user in the foreground. And waits for the user switch to be complete.
+     **/
+    private void switchUser(UserHandle user) {
+        final int userId = user.getIdentifier();
+        Slog.d(TAG, "Switching to user " + userId);
+
+        mUserSwitchWaiter.runThenWaitUntilSwitchCompleted(userId, () -> {
+            assertWithMessage("Could not start switching to user " + userId)
+                    .that(mActivityManager.switchUser(user)).isTrue();
+        }, /* onFail= */ () -> {
+            throw new AssertionError("Could not complete switching to user " + userId);
+        });
+    }
+
+    /**
+     * Starts the given user in the foreground. And waits for the user switch to be complete.
+     **/
+    private void switchUser(int userId) {
+        switchUserThenRun(userId, null);
+    }
+
+    /**
+     * Starts the given user in the foreground. And runs the given Runnable right after
+     * am.switchUser call, before waiting for the actual user switch to be complete.
+     **/
+    private void switchUserThenRun(int userId, Runnable runAfterSwitchBeforeWait) {
+        Slog.d(TAG, "Switching to user " + userId);
+        mUserSwitchWaiter.runThenWaitUntilSwitchCompleted(userId, () -> {
+            // Start switching to user
+            assertWithMessage("Could not start switching to user " + userId)
+                    .that(mActivityManager.switchUser(userId)).isTrue();
+
+            // While the user switch is happening, call runAfterSwitchBeforeWait.
+            if (runAfterSwitchBeforeWait != null) {
+                runAfterSwitchBeforeWait.run();
             }
-            long time = System.currentTimeMillis();
-            try {
-                mUserSwitchLock.wait(SWITCH_USER_TIMEOUT_MILLIS);
-            } catch (InterruptedException ie) {
-                Thread.currentThread().interrupt();
-                return;
-            }
-            if (System.currentTimeMillis() - time > SWITCH_USER_TIMEOUT_MILLIS) {
-                fail("Timeout waiting for the user switch to u"
-                        + (ignoreHandle ? userId : user.getIdentifier()));
-            }
-        }
+        }, () -> fail("Could not complete switching to user " + userId));
     }
 
     private void removeUser(UserHandle user) {
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserSwitchWaiter.java b/services/tests/servicestests/src/com/android/server/pm/UserSwitchWaiter.java
new file mode 100644
index 0000000..d948570
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/pm/UserSwitchWaiter.java
@@ -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.server.pm;
+
+import android.app.ActivityManager;
+import android.app.IActivityManager;
+import android.app.IUserSwitchObserver;
+import android.app.UserSwitchObserver;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.util.FunctionalUtils;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+public class UserSwitchWaiter implements Closeable {
+
+    private final String mTag;
+    private final int mTimeoutInSecond;
+    private final IActivityManager mActivityManager;
+    private final IUserSwitchObserver mUserSwitchObserver = new UserSwitchObserver() {
+        @Override
+        public void onUserSwitchComplete(int newUserId) {
+            getSemaphoreSwitchComplete(newUserId).release();
+        }
+
+        @Override
+        public void onLockedBootComplete(int newUserId) {
+            getSemaphoreBootComplete(newUserId).release();
+        }
+    };
+
+    private final Map<Integer, Semaphore> mSemaphoresMapSwitchComplete = new ConcurrentHashMap<>();
+    private Semaphore getSemaphoreSwitchComplete(final int userId) {
+        return mSemaphoresMapSwitchComplete.computeIfAbsent(userId,
+                (Integer absentKey) -> new Semaphore(0));
+    }
+
+    private final Map<Integer, Semaphore> mSemaphoresMapBootComplete = new ConcurrentHashMap<>();
+    private Semaphore getSemaphoreBootComplete(final int userId) {
+        return mSemaphoresMapBootComplete.computeIfAbsent(userId,
+                (Integer absentKey) -> new Semaphore(0));
+    }
+
+    public UserSwitchWaiter(String tag, int timeoutInSecond) throws RemoteException {
+        mTag = tag;
+        mTimeoutInSecond = timeoutInSecond;
+        mActivityManager = ActivityManager.getService();
+
+        mActivityManager.registerUserSwitchObserver(mUserSwitchObserver, mTag);
+    }
+
+    @Override
+    public void close() throws IOException {
+        try {
+            mActivityManager.unregisterUserSwitchObserver(mUserSwitchObserver);
+        } catch (RemoteException e) {
+            Log.e(mTag, "Failed to unregister user switch observer", e);
+        }
+    }
+
+    public void runThenWaitUntilSwitchCompleted(int userId,
+            FunctionalUtils.ThrowingRunnable runnable, Runnable onFail) {
+        final Semaphore semaphore = getSemaphoreSwitchComplete(userId);
+        semaphore.drainPermits();
+        runnable.run();
+        waitForSemaphore(semaphore, onFail);
+    }
+
+    public void runThenWaitUntilBootCompleted(int userId,
+            FunctionalUtils.ThrowingRunnable runnable, Runnable onFail) {
+        final Semaphore semaphore = getSemaphoreBootComplete(userId);
+        semaphore.drainPermits();
+        runnable.run();
+        waitForSemaphore(semaphore, onFail);
+    }
+
+    private void waitForSemaphore(Semaphore semaphore, Runnable onFail) {
+        boolean success = false;
+        try {
+            success = semaphore.tryAcquire(mTimeoutInSecond, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            Log.e(mTag, "Thread interrupted unexpectedly.", e);
+        }
+        if (!success && onFail != null) {
+            onFail.run();
+        }
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index bc23fa3..78707d6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -2066,7 +2066,8 @@
         // Update the forced size and density in settings and the unique id to simualate a display
         // remap.
         dc.mWmService.mDisplayWindowSettings.setForcedSize(dc, forcedWidth, forcedHeight);
-        dc.mWmService.mDisplayWindowSettings.setForcedDensity(dc, forcedDensity, 0 /* userId */);
+        dc.mWmService.mDisplayWindowSettings.setForcedDensity(displayInfo, forcedDensity,
+                0 /* userId */);
         dc.mCurrentUniqueDisplayId = mDisplayInfo.uniqueId + "-test";
         // Trigger display changed.
         dc.onDisplayChanged();
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
index c398a0a..fb4f2ee 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
@@ -236,8 +236,8 @@
 
     @Test
     public void testSetForcedDensity() {
-        mDisplayWindowSettings.setForcedDensity(mSecondaryDisplay, 600 /* density */,
-                0 /* userId */);
+        mDisplayWindowSettings.setForcedDensity(mSecondaryDisplay.getDisplayInfo(),
+                600 /* density */, 0 /* userId */);
         mDisplayWindowSettings.applySettingsToDisplayLocked(mSecondaryDisplay);
 
         assertEquals(600 /* density */, mSecondaryDisplay.mBaseDisplayDensity);
@@ -439,8 +439,9 @@
     public void testDisplayWindowSettingsAppliedOnDisplayReady() {
         // Set forced densities for two displays in DisplayWindowSettings
         final DisplayContent dc = createMockSimulatedDisplay();
-        mDisplayWindowSettings.setForcedDensity(mPrimaryDisplay, 123, 0 /* userId */);
-        mDisplayWindowSettings.setForcedDensity(dc, 456, 0 /* userId */);
+        mDisplayWindowSettings.setForcedDensity(mPrimaryDisplay.getDisplayInfo(), 123,
+                0 /* userId */);
+        mDisplayWindowSettings.setForcedDensity(dc.getDisplayInfo(), 456, 0 /* userId */);
 
         // Apply settings to displays - the settings will be stored, but config will not be
         // recalculated immediately.
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
index bf1d1fa..d31ae6a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
@@ -203,6 +203,7 @@
             }
 
             final int displayId = SystemServicesTestRule.sNextDisplayId++;
+            mInfo.displayId = displayId;
             final Display display = new Display(DisplayManagerGlobal.getInstance(), displayId,
                     mInfo, DEFAULT_DISPLAY_ADJUSTMENTS);
             final TestDisplayContent newDisplay = createInternal(display);
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToAppTest.kt
index c735be0..522f9e7 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToAppTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToAppTest.kt
@@ -48,7 +48,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CloseImeAutoOpenWindowToAppTest(flicker: FlickerTest) : BaseTest(flicker) {
+open class CloseImeAutoOpenWindowToAppTest(flicker: FlickerTest) : BaseTest(flicker) {
     private val testApp = ImeAppAutoFocusHelper(instrumentation, flicker.scenario.startRotation)
 
     /** {@inheritDoc} */
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToAppTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToAppTestCfArm.kt
new file mode 100644
index 0000000..70443d8
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToAppTestCfArm.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.server.wm.flicker.ime
+
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class CloseImeAutoOpenWindowToAppTestCfArm(flicker: FlickerTest) :
+    CloseImeAutoOpenWindowToAppTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTest.kt
index 4024f56..2081424 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTest.kt
@@ -48,7 +48,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CloseImeAutoOpenWindowToHomeTest(flicker: FlickerTest) : BaseTest(flicker) {
+open class CloseImeAutoOpenWindowToHomeTest(flicker: FlickerTest) : BaseTest(flicker) {
     private val testApp = ImeAppAutoFocusHelper(instrumentation, flicker.scenario.startRotation)
 
     /** {@inheritDoc} */
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTestCfArm.kt
new file mode 100644
index 0000000..2ffc757
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeAutoOpenWindowToHomeTestCfArm.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.server.wm.flicker.ime
+
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class CloseImeAutoOpenWindowToHomeTestCfArm(flicker: FlickerTest) :
+    CloseImeAutoOpenWindowToHomeTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTest.kt
index 098c082..13feeb1 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTest.kt
@@ -43,7 +43,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CloseImeWindowToAppTest(flicker: FlickerTest) : BaseTest(flicker) {
+open class CloseImeWindowToAppTest(flicker: FlickerTest) : BaseTest(flicker) {
     private val testApp = ImeAppHelper(instrumentation)
 
     /** {@inheritDoc} */
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTestCfArm.kt
new file mode 100644
index 0000000..f40f5e6
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTestCfArm.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.server.wm.flicker.ime
+
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class CloseImeWindowToAppTestCfArm(flicker: FlickerTest) : CloseImeWindowToAppTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTest.kt
index f110e54..840575a 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTest.kt
@@ -41,7 +41,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class CloseImeWindowToHomeTest(flicker: FlickerTest) : BaseTest(flicker) {
+open class CloseImeWindowToHomeTest(flicker: FlickerTest) : BaseTest(flicker) {
     private val testApp = ImeAppHelper(instrumentation)
 
     /** {@inheritDoc} */
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTestCfArm.kt
new file mode 100644
index 0000000..87d9abd
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTestCfArm.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.server.wm.flicker.ime
+
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class CloseImeWindowToHomeTestCfArm(flicker: FlickerTest) : CloseImeWindowToHomeTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeOnStartTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeOnStartTest.kt
index 4891901..6685cf7 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeOnStartTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeOnStartTest.kt
@@ -72,7 +72,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class LaunchAppShowImeOnStartTest(flicker: FlickerTest) : BaseTest(flicker) {
+open class LaunchAppShowImeOnStartTest(flicker: FlickerTest) : BaseTest(flicker) {
     private val testApp = ImeAppAutoFocusHelper(instrumentation, flicker.scenario.startRotation)
     private val initializeApp = ImeStateInitializeHelper(instrumentation)
 
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeOnStartTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeOnStartTestCfArm.kt
new file mode 100644
index 0000000..04cae53
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/LaunchAppShowImeOnStartTestCfArm.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.server.wm.flicker.ime
+
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class LaunchAppShowImeOnStartTestCfArm(flicker: FlickerTest) :
+    LaunchAppShowImeOnStartTest(flicker)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTest.kt
index 0e732b5..d41843f 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTest.kt
@@ -45,7 +45,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class OpenImeWindowFromFixedOrientationAppTest(flicker: FlickerTest) : BaseTest(flicker) {
+open class OpenImeWindowFromFixedOrientationAppTest(flicker: FlickerTest) : BaseTest(flicker) {
     private val imeTestApp = ImeAppAutoFocusHelper(instrumentation, flicker.scenario.startRotation)
 
     /** {@inheritDoc} */
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTestCfArm.kt
new file mode 100644
index 0000000..0e6cb53
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTestCfArm.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.server.wm.flicker.ime
+
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class OpenImeWindowFromFixedOrientationAppTestCfArm(flicker: FlickerTest) :
+    OpenImeWindowFromFixedOrientationAppTest(flicker)