Merge "Update transition type to TRANSIT_TO_FRONT in test" into main
diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp
index 56a69b4..e5e0ad3 100644
--- a/api/StubLibraries.bp
+++ b/api/StubLibraries.bp
@@ -86,6 +86,9 @@
 
 droidstubs {
     name: "system-api-stubs-docs-non-updatable",
+    srcs: [
+        ":framework-minus-apex-aconfig-srcjars",
+    ],
     defaults: [
         "android-non-updatable-stubs-defaults",
         "module-classpath-stubs-defaults",
@@ -126,6 +129,9 @@
 
 droidstubs {
     name: "test-api-stubs-docs-non-updatable",
+    srcs: [
+        ":framework-minus-apex-aconfig-srcjars",
+    ],
     defaults: [
         "android-non-updatable-stubs-defaults",
         "module-classpath-stubs-defaults",
@@ -173,6 +179,9 @@
 
 droidstubs {
     name: "module-lib-api-stubs-docs-non-updatable",
+    srcs: [
+        ":framework-minus-apex-aconfig-srcjars",
+    ],
     defaults: [
         "android-non-updatable-stubs-defaults",
         "module-classpath-stubs-defaults",
diff --git a/core/api/current.txt b/core/api/current.txt
index d8e3abf..955858b 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -9682,6 +9682,7 @@
     method public int describeContents();
     method public int getDeviceId();
     method @FlaggedApi("android.companion.virtual.flags.vdm_public_apis") @NonNull public int[] getDisplayIds();
+    method @FlaggedApi("android.companion.virtual.flags.vdm_public_apis") @Nullable public CharSequence getDisplayName();
     method @Nullable public String getName();
     method @Nullable public String getPersistentDeviceId();
     method @FlaggedApi("android.companion.virtual.flags.vdm_public_apis") public boolean hasCustomSensorSupport();
@@ -39008,6 +39009,7 @@
     method @Nullable public java.util.Date getKeyValidityStart();
     method @NonNull public String getKeystoreAlias();
     method public int getMaxUsageCount();
+    method @FlaggedApi("MGF1_DIGEST_SETTER") @NonNull public java.util.Set<java.lang.String> getMgf1Digests();
     method public int getPurposes();
     method @NonNull public String[] getSignaturePaddings();
     method public int getUserAuthenticationType();
@@ -39015,6 +39017,7 @@
     method public boolean isDevicePropertiesAttestationIncluded();
     method @NonNull public boolean isDigestsSpecified();
     method public boolean isInvalidatedByBiometricEnrollment();
+    method @FlaggedApi("MGF1_DIGEST_SETTER") @NonNull public boolean isMgf1DigestsSpecified();
     method public boolean isRandomizedEncryptionRequired();
     method public boolean isStrongBoxBacked();
     method public boolean isUnlockedDeviceRequired();
@@ -39046,6 +39049,7 @@
     method @NonNull public android.security.keystore.KeyGenParameterSpec.Builder setKeyValidityForOriginationEnd(java.util.Date);
     method @NonNull public android.security.keystore.KeyGenParameterSpec.Builder setKeyValidityStart(java.util.Date);
     method @NonNull public android.security.keystore.KeyGenParameterSpec.Builder setMaxUsageCount(int);
+    method @FlaggedApi("MGF1_DIGEST_SETTER") @NonNull public android.security.keystore.KeyGenParameterSpec.Builder setMgf1Digests(@Nullable java.lang.String...);
     method @NonNull public android.security.keystore.KeyGenParameterSpec.Builder setRandomizedEncryptionRequired(boolean);
     method @NonNull public android.security.keystore.KeyGenParameterSpec.Builder setSignaturePaddings(java.lang.String...);
     method @NonNull public android.security.keystore.KeyGenParameterSpec.Builder setUnlockedDeviceRequired(boolean);
@@ -39150,12 +39154,14 @@
     method @Nullable public java.util.Date getKeyValidityForOriginationEnd();
     method @Nullable public java.util.Date getKeyValidityStart();
     method public int getMaxUsageCount();
+    method @FlaggedApi("MGF1_DIGEST_SETTER") @NonNull public java.util.Set<java.lang.String> getMgf1Digests();
     method public int getPurposes();
     method @NonNull public String[] getSignaturePaddings();
     method public int getUserAuthenticationType();
     method public int getUserAuthenticationValidityDurationSeconds();
     method public boolean isDigestsSpecified();
     method public boolean isInvalidatedByBiometricEnrollment();
+    method @FlaggedApi("MGF1_DIGEST_SETTER") @NonNull public boolean isMgf1DigestsSpecified();
     method public boolean isRandomizedEncryptionRequired();
     method public boolean isUnlockedDeviceRequired();
     method public boolean isUserAuthenticationRequired();
@@ -39177,6 +39183,7 @@
     method @NonNull public android.security.keystore.KeyProtection.Builder setKeyValidityForOriginationEnd(java.util.Date);
     method @NonNull public android.security.keystore.KeyProtection.Builder setKeyValidityStart(java.util.Date);
     method @NonNull public android.security.keystore.KeyProtection.Builder setMaxUsageCount(int);
+    method @FlaggedApi("MGF1_DIGEST_SETTER") @NonNull public android.security.keystore.KeyProtection.Builder setMgf1Digests(@Nullable java.lang.String...);
     method @NonNull public android.security.keystore.KeyProtection.Builder setRandomizedEncryptionRequired(boolean);
     method @NonNull public android.security.keystore.KeyProtection.Builder setSignaturePaddings(java.lang.String...);
     method @NonNull public android.security.keystore.KeyProtection.Builder setUnlockedDeviceRequired(boolean);
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index 9b8d2b4..052d614 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -556,24 +556,24 @@
 
 package android.se.omapi {
 
-  @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public class SeFrameworkInitializer {
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @Nullable public static android.se.omapi.SeServiceManager getSeServiceManager();
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public static void setSeServiceManager(@NonNull android.se.omapi.SeServiceManager);
+  @FlaggedApi("android.nfc.enable_nfc_mainline") public class SeFrameworkInitializer {
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @Nullable public static android.se.omapi.SeServiceManager getSeServiceManager();
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") public static void setSeServiceManager(@NonNull android.se.omapi.SeServiceManager);
   }
 
-  @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public class SeServiceManager {
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public android.se.omapi.SeServiceManager.ServiceRegisterer getSeManagerServiceRegisterer();
+  @FlaggedApi("android.nfc.enable_nfc_mainline") public class SeServiceManager {
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public android.se.omapi.SeServiceManager.ServiceRegisterer getSeManagerServiceRegisterer();
   }
 
-  @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public static class SeServiceManager.ServiceNotFoundException extends java.lang.Exception {
-    ctor @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public SeServiceManager.ServiceNotFoundException(@NonNull String);
+  @FlaggedApi("android.nfc.enable_nfc_mainline") public static class SeServiceManager.ServiceNotFoundException extends java.lang.Exception {
+    ctor @FlaggedApi("android.nfc.enable_nfc_mainline") public SeServiceManager.ServiceNotFoundException(@NonNull String);
   }
 
-  @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public static final class SeServiceManager.ServiceRegisterer {
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @Nullable public android.os.IBinder get();
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public android.os.IBinder getOrThrow() throws android.se.omapi.SeServiceManager.ServiceNotFoundException;
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public void register(@NonNull android.os.IBinder);
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @Nullable public android.os.IBinder tryGet();
+  @FlaggedApi("android.nfc.enable_nfc_mainline") public static final class SeServiceManager.ServiceRegisterer {
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @Nullable public android.os.IBinder get();
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public android.os.IBinder getOrThrow() throws android.se.omapi.SeServiceManager.ServiceNotFoundException;
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") public void register(@NonNull android.os.IBinder);
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @Nullable public android.os.IBinder tryGet();
   }
 
 }
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 220482d..1d88e00 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -540,7 +540,7 @@
     method @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public void addOnUidImportanceListener(android.app.ActivityManager.OnUidImportanceListener, int);
     method @RequiresPermission(android.Manifest.permission.FORCE_STOP_PACKAGES) public void forceStopPackage(String);
     method @RequiresPermission(anyOf={"android.permission.INTERACT_ACROSS_USERS", "android.permission.INTERACT_ACROSS_USERS_FULL"}) public static int getCurrentUser();
-    method @FlaggedApi(Flags.FLAG_APP_START_INFO) @NonNull @RequiresPermission(android.Manifest.permission.DUMP) public java.util.List<android.app.ApplicationStartInfo> getExternalHistoricalProcessStartReasons(@NonNull String, @IntRange(from=0) int);
+    method @FlaggedApi("android.app.app_start_info") @NonNull @RequiresPermission(android.Manifest.permission.DUMP) public java.util.List<android.app.ApplicationStartInfo> getExternalHistoricalProcessStartReasons(@NonNull String, @IntRange(from=0) int);
     method @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public int getPackageImportance(String);
     method @NonNull public java.util.Collection<java.util.Locale> getSupportedLocales();
     method @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public int getUidImportance(int);
@@ -3193,7 +3193,7 @@
 
   public static class VirtualDeviceManager.VirtualDevice implements java.lang.AutoCloseable {
     method public void addActivityListener(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.ActivityListener);
-    method @FlaggedApi(Flags.FLAG_DYNAMIC_POLICY) @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void addActivityPolicyExemption(@NonNull android.content.ComponentName);
+    method @FlaggedApi("android.companion.virtual.flags.dynamic_policy") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void addActivityPolicyExemption(@NonNull android.content.ComponentName);
     method public void addSoundEffectListener(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.SoundEffectListener);
     method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
     method @NonNull public android.content.Context createContext();
@@ -3214,9 +3214,9 @@
     method public void launchPendingIntent(int, @NonNull android.app.PendingIntent, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer);
     method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void registerIntentInterceptor(@NonNull android.content.IntentFilter, @NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.IntentInterceptorCallback);
     method public void removeActivityListener(@NonNull android.companion.virtual.VirtualDeviceManager.ActivityListener);
-    method @FlaggedApi(Flags.FLAG_DYNAMIC_POLICY) @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void removeActivityPolicyExemption(@NonNull android.content.ComponentName);
+    method @FlaggedApi("android.companion.virtual.flags.dynamic_policy") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void removeActivityPolicyExemption(@NonNull android.content.ComponentName);
     method public void removeSoundEffectListener(@NonNull android.companion.virtual.VirtualDeviceManager.SoundEffectListener);
-    method @FlaggedApi(Flags.FLAG_DYNAMIC_POLICY) @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setDevicePolicy(int, int);
+    method @FlaggedApi("android.companion.virtual.flags.dynamic_policy") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setDevicePolicy(int, int);
     method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setShowPointerIcon(boolean);
     method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void unregisterIntentInterceptor(@NonNull android.companion.virtual.VirtualDeviceManager.IntentInterceptorCallback);
   }
@@ -3232,7 +3232,7 @@
     method public int getDefaultActivityPolicy();
     method public int getDefaultNavigationPolicy();
     method public int getDevicePolicy(int);
-    method @FlaggedApi(Flags.FLAG_VDM_CUSTOM_HOME) @Nullable public android.content.ComponentName getHomeComponent();
+    method @FlaggedApi("android.companion.virtual.flags.vdm_custom_home") @Nullable public android.content.ComponentName getHomeComponent();
     method public int getLockState();
     method @Nullable public String getName();
     method @NonNull public java.util.Set<android.os.UserHandle> getUsersWithMatchingAccounts();
@@ -3247,7 +3247,7 @@
     field public static final int LOCK_STATE_DEFAULT = 0; // 0x0
     field public static final int NAVIGATION_POLICY_DEFAULT_ALLOWED = 0; // 0x0
     field public static final int NAVIGATION_POLICY_DEFAULT_BLOCKED = 1; // 0x1
-    field @FlaggedApi(Flags.FLAG_DYNAMIC_POLICY) public static final int POLICY_TYPE_ACTIVITY = 3; // 0x3
+    field @FlaggedApi("android.companion.virtual.flags.dynamic_policy") public static final int POLICY_TYPE_ACTIVITY = 3; // 0x3
     field public static final int POLICY_TYPE_AUDIO = 1; // 0x1
     field public static final int POLICY_TYPE_RECENTS = 2; // 0x2
     field public static final int POLICY_TYPE_SENSORS = 0; // 0x0
@@ -3264,7 +3264,7 @@
     method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setBlockedActivities(@NonNull java.util.Set<android.content.ComponentName>);
     method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setBlockedCrossTaskNavigations(@NonNull java.util.Set<android.content.ComponentName>);
     method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setDevicePolicy(int, int);
-    method @FlaggedApi(Flags.FLAG_VDM_CUSTOM_HOME) @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setHomeComponent(@Nullable android.content.ComponentName);
+    method @FlaggedApi("android.companion.virtual.flags.vdm_custom_home") @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setHomeComponent(@Nullable android.content.ComponentName);
     method @NonNull @RequiresPermission(value=android.Manifest.permission.ADD_ALWAYS_UNLOCKED_DISPLAY, conditional=true) public android.companion.virtual.VirtualDeviceParams.Builder setLockState(int);
     method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setName(@NonNull String);
     method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setUsersWithMatchingAccounts(@NonNull java.util.Set<android.os.UserHandle>);
@@ -3805,8 +3805,8 @@
   public class PackageInstaller {
     method @NonNull public android.content.pm.PackageInstaller.InstallInfo readInstallInfo(@NonNull java.io.File, int) throws android.content.pm.PackageInstaller.PackageParsingException;
     method @NonNull public android.content.pm.PackageInstaller.InstallInfo readInstallInfo(@NonNull android.os.ParcelFileDescriptor, @Nullable String, int) throws android.content.pm.PackageInstaller.PackageParsingException;
-    method @FlaggedApi(Flags.FLAG_ARCHIVING) @RequiresPermission(anyOf={android.Manifest.permission.DELETE_PACKAGES, android.Manifest.permission.REQUEST_DELETE_PACKAGES}) public void requestArchive(@NonNull String, @NonNull android.content.IntentSender) throws android.content.pm.PackageManager.NameNotFoundException;
-    method @FlaggedApi(Flags.FLAG_ARCHIVING) @RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES, android.Manifest.permission.REQUEST_INSTALL_PACKAGES}) public void requestUnarchive(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException;
+    method @FlaggedApi("android.content.pm.archiving") @RequiresPermission(anyOf={android.Manifest.permission.DELETE_PACKAGES, android.Manifest.permission.REQUEST_DELETE_PACKAGES}) public void requestArchive(@NonNull String, @NonNull android.content.IntentSender) throws android.content.pm.PackageManager.NameNotFoundException;
+    method @FlaggedApi("android.content.pm.archiving") @RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES, android.Manifest.permission.REQUEST_INSTALL_PACKAGES}) public void requestUnarchive(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException;
     method @RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES) public void setPermissionsResult(int, boolean);
     field public static final String ACTION_CONFIRM_INSTALL = "android.content.pm.action.CONFIRM_INSTALL";
     field public static final String ACTION_CONFIRM_PRE_APPROVAL = "android.content.pm.action.CONFIRM_PRE_APPROVAL";
@@ -3817,8 +3817,8 @@
     field public static final String EXTRA_DATA_LOADER_TYPE = "android.content.pm.extra.DATA_LOADER_TYPE";
     field public static final String EXTRA_LEGACY_STATUS = "android.content.pm.extra.LEGACY_STATUS";
     field public static final String EXTRA_RESOLVED_BASE_PATH = "android.content.pm.extra.RESOLVED_BASE_PATH";
-    field @FlaggedApi(Flags.FLAG_ARCHIVING) public static final String EXTRA_UNARCHIVE_ALL_USERS = "android.content.pm.extra.UNARCHIVE_ALL_USERS";
-    field @FlaggedApi(Flags.FLAG_ARCHIVING) public static final String EXTRA_UNARCHIVE_PACKAGE_NAME = "android.content.pm.extra.UNARCHIVE_PACKAGE_NAME";
+    field @FlaggedApi("android.content.pm.archiving") public static final String EXTRA_UNARCHIVE_ALL_USERS = "android.content.pm.extra.UNARCHIVE_ALL_USERS";
+    field @FlaggedApi("android.content.pm.archiving") public static final String EXTRA_UNARCHIVE_PACKAGE_NAME = "android.content.pm.extra.UNARCHIVE_PACKAGE_NAME";
     field public static final int LOCATION_DATA_APP = 0; // 0x0
     field public static final int LOCATION_MEDIA_DATA = 2; // 0x2
     field public static final int LOCATION_MEDIA_OBB = 1; // 0x1
@@ -3879,7 +3879,7 @@
     method public static void forceSafeLabels();
     method @Deprecated @NonNull public CharSequence loadSafeLabel(@NonNull android.content.pm.PackageManager);
     method @NonNull public CharSequence loadSafeLabel(@NonNull android.content.pm.PackageManager, @FloatRange(from=0) float, int);
-    field @FlaggedApi(Flags.FLAG_ARCHIVING) public boolean isArchived;
+    field @FlaggedApi("android.content.pm.archiving") public boolean isArchived;
   }
 
   public abstract class PackageManager {
@@ -3929,7 +3929,7 @@
     method @RequiresPermission(android.Manifest.permission.SET_HARMFUL_APP_WARNINGS) public void setHarmfulAppWarning(@NonNull String, @Nullable CharSequence);
     method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.SUSPEND_APPS) public String[] setPackagesSuspended(@Nullable String[], boolean, @Nullable android.os.PersistableBundle, @Nullable android.os.PersistableBundle, @Nullable String);
     method @Nullable @RequiresPermission(value=android.Manifest.permission.SUSPEND_APPS, conditional=true) public String[] setPackagesSuspended(@Nullable String[], boolean, @Nullable android.os.PersistableBundle, @Nullable android.os.PersistableBundle, @Nullable android.content.pm.SuspendDialogInfo);
-    method @FlaggedApi(android.content.pm.Flags.FLAG_QUARANTINED_ENABLED) @Nullable @RequiresPermission(value=android.Manifest.permission.SUSPEND_APPS, conditional=true) public String[] setPackagesSuspended(@Nullable String[], boolean, @Nullable android.os.PersistableBundle, @Nullable android.os.PersistableBundle, @Nullable android.content.pm.SuspendDialogInfo, int);
+    method @FlaggedApi("android.content.pm.quarantined_enabled") @Nullable @RequiresPermission(value=android.Manifest.permission.SUSPEND_APPS, conditional=true) public String[] setPackagesSuspended(@Nullable String[], boolean, @Nullable android.os.PersistableBundle, @Nullable android.os.PersistableBundle, @Nullable android.content.pm.SuspendDialogInfo, int);
     method @RequiresPermission(value=android.Manifest.permission.CHANGE_COMPONENT_ENABLED_STATE, conditional=true) public void setSyntheticAppDetailsActivityEnabled(@NonNull String, boolean);
     method public void setSystemAppState(@NonNull String, int);
     method @RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES) public abstract void setUpdateAvailable(@NonNull String, boolean);
@@ -3980,7 +3980,7 @@
     field public static final int FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED = 512; // 0x200
     field public static final int FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED = 256; // 0x100
     field public static final int FLAG_PERMISSION_USER_SET = 1; // 0x1
-    field @FlaggedApi(android.content.pm.Flags.FLAG_QUARANTINED_ENABLED) public static final int FLAG_SUSPEND_QUARANTINED = 1; // 0x1
+    field @FlaggedApi("android.content.pm.quarantined_enabled") public static final int FLAG_SUSPEND_QUARANTINED = 1; // 0x1
     field public static final int INSTALL_FAILED_ALREADY_EXISTS = -1; // 0xffffffff
     field public static final int INSTALL_FAILED_CONFLICTING_PROVIDER = -13; // 0xfffffff3
     field public static final int INSTALL_FAILED_CONTAINER_ERROR = -18; // 0xffffffee
@@ -9632,67 +9632,67 @@
 
 package android.nfc.cardemulation {
 
-  @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public final class AidGroup implements android.os.Parcelable {
-    ctor @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public AidGroup(@NonNull java.util.List<java.lang.String>, @Nullable String);
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @Nullable public static android.nfc.cardemulation.AidGroup createFromXml(@NonNull org.xmlpull.v1.XmlPullParser) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public int describeContents();
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public void dump(@NonNull android.util.proto.ProtoOutputStream);
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public java.util.List<java.lang.String> getAids();
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public String getCategory();
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public void writeAsXml(@NonNull org.xmlpull.v1.XmlSerializer) throws java.io.IOException;
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public static final android.os.Parcelable.Creator<android.nfc.cardemulation.AidGroup> CREATOR;
+  @FlaggedApi("android.nfc.enable_nfc_mainline") public final class AidGroup implements android.os.Parcelable {
+    ctor @FlaggedApi("android.nfc.enable_nfc_mainline") public AidGroup(@NonNull java.util.List<java.lang.String>, @Nullable String);
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @Nullable public static android.nfc.cardemulation.AidGroup createFromXml(@NonNull org.xmlpull.v1.XmlPullParser) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") public int describeContents();
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") public void dump(@NonNull android.util.proto.ProtoOutputStream);
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public java.util.List<java.lang.String> getAids();
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public String getCategory();
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") public void writeAsXml(@NonNull org.xmlpull.v1.XmlSerializer) throws java.io.IOException;
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public static final android.os.Parcelable.Creator<android.nfc.cardemulation.AidGroup> CREATOR;
   }
 
-  @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public final class ApduServiceInfo implements android.os.Parcelable {
-    ctor @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public ApduServiceInfo(@NonNull android.content.pm.PackageManager, @NonNull android.content.pm.ResolveInfo, boolean) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public int describeContents();
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public void dump(@NonNull android.os.ParcelFileDescriptor, @NonNull java.io.PrintWriter, @NonNull String[]);
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public void dumpDebug(@NonNull android.util.proto.ProtoOutputStream);
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public java.util.List<android.nfc.cardemulation.AidGroup> getAidGroups();
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public java.util.List<java.lang.String> getAids();
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public String getCategoryForAid(@NonNull String);
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public android.content.ComponentName getComponent();
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public String getDescription();
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public android.nfc.cardemulation.AidGroup getDynamicAidGroupForCategory(@NonNull String);
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @Nullable public String getOffHostSecureElement();
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public java.util.List<java.lang.String> getPrefixAids();
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public String getSettingsActivityName();
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public java.util.List<java.lang.String> getSubsetAids();
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public int getUid();
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public boolean hasCategory(@NonNull String);
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public boolean isOnHost();
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public CharSequence loadAppLabel(@NonNull android.content.pm.PackageManager);
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public android.graphics.drawable.Drawable loadBanner(@NonNull android.content.pm.PackageManager);
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public android.graphics.drawable.Drawable loadIcon(@NonNull android.content.pm.PackageManager);
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public CharSequence loadLabel(@NonNull android.content.pm.PackageManager);
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public boolean removeDynamicAidGroupForCategory(@NonNull String);
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public boolean requiresScreenOn();
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public boolean requiresUnlock();
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public void resetOffHostSecureElement();
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public void setDynamicAidGroup(@NonNull android.nfc.cardemulation.AidGroup);
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public void setOffHostSecureElement(@NonNull String);
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public static final android.os.Parcelable.Creator<android.nfc.cardemulation.ApduServiceInfo> CREATOR;
+  @FlaggedApi("android.nfc.enable_nfc_mainline") public final class ApduServiceInfo implements android.os.Parcelable {
+    ctor @FlaggedApi("android.nfc.enable_nfc_mainline") public ApduServiceInfo(@NonNull android.content.pm.PackageManager, @NonNull android.content.pm.ResolveInfo, boolean) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") public int describeContents();
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") public void dump(@NonNull android.os.ParcelFileDescriptor, @NonNull java.io.PrintWriter, @NonNull String[]);
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") public void dumpDebug(@NonNull android.util.proto.ProtoOutputStream);
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public java.util.List<android.nfc.cardemulation.AidGroup> getAidGroups();
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public java.util.List<java.lang.String> getAids();
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public String getCategoryForAid(@NonNull String);
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public android.content.ComponentName getComponent();
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public String getDescription();
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public android.nfc.cardemulation.AidGroup getDynamicAidGroupForCategory(@NonNull String);
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @Nullable public String getOffHostSecureElement();
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public java.util.List<java.lang.String> getPrefixAids();
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public String getSettingsActivityName();
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public java.util.List<java.lang.String> getSubsetAids();
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") public int getUid();
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") public boolean hasCategory(@NonNull String);
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") public boolean isOnHost();
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public CharSequence loadAppLabel(@NonNull android.content.pm.PackageManager);
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public android.graphics.drawable.Drawable loadBanner(@NonNull android.content.pm.PackageManager);
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public android.graphics.drawable.Drawable loadIcon(@NonNull android.content.pm.PackageManager);
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public CharSequence loadLabel(@NonNull android.content.pm.PackageManager);
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public boolean removeDynamicAidGroupForCategory(@NonNull String);
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") public boolean requiresScreenOn();
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") public boolean requiresUnlock();
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") public void resetOffHostSecureElement();
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") public void setDynamicAidGroup(@NonNull android.nfc.cardemulation.AidGroup);
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") public void setOffHostSecureElement(@NonNull String);
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public static final android.os.Parcelable.Creator<android.nfc.cardemulation.ApduServiceInfo> CREATOR;
   }
 
-  @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public final class NfcFServiceInfo implements android.os.Parcelable {
-    ctor @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public NfcFServiceInfo(@NonNull android.content.pm.PackageManager, @NonNull android.content.pm.ResolveInfo) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public int describeContents();
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public void dump(@NonNull android.os.ParcelFileDescriptor, @NonNull java.io.PrintWriter, @NonNull String[]);
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public void dumpDebug(@NonNull android.util.proto.ProtoOutputStream);
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public android.content.ComponentName getComponent();
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public String getDescription();
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public String getNfcid2();
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public String getSystemCode();
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public String getT3tPmm();
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public int getUid();
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public android.graphics.drawable.Drawable loadIcon(@NonNull android.content.pm.PackageManager);
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public CharSequence loadLabel(@NonNull android.content.pm.PackageManager);
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public void setDynamicNfcid2(@NonNull String);
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public void setDynamicSystemCode(@NonNull String);
-    method @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public static final android.os.Parcelable.Creator<android.nfc.cardemulation.NfcFServiceInfo> CREATOR;
+  @FlaggedApi("android.nfc.enable_nfc_mainline") public final class NfcFServiceInfo implements android.os.Parcelable {
+    ctor @FlaggedApi("android.nfc.enable_nfc_mainline") public NfcFServiceInfo(@NonNull android.content.pm.PackageManager, @NonNull android.content.pm.ResolveInfo) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") public int describeContents();
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") public void dump(@NonNull android.os.ParcelFileDescriptor, @NonNull java.io.PrintWriter, @NonNull String[]);
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") public void dumpDebug(@NonNull android.util.proto.ProtoOutputStream);
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public android.content.ComponentName getComponent();
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public String getDescription();
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public String getNfcid2();
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public String getSystemCode();
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public String getT3tPmm();
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") public int getUid();
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public android.graphics.drawable.Drawable loadIcon(@NonNull android.content.pm.PackageManager);
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public CharSequence loadLabel(@NonNull android.content.pm.PackageManager);
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") public void setDynamicNfcid2(@NonNull String);
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") public void setDynamicSystemCode(@NonNull String);
+    method @FlaggedApi("android.nfc.enable_nfc_mainline") public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public static final android.os.Parcelable.Creator<android.nfc.cardemulation.NfcFServiceInfo> CREATOR;
   }
 
 }
@@ -10705,13 +10705,13 @@
     method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_APP_HIBERNATION) public void onGetUnusedAppCount(@NonNull java.util.function.IntConsumer);
     method @BinderThread public abstract void onGrantOrUpgradeDefaultRuntimePermissions(@NonNull Runnable);
     method @Deprecated @BinderThread public void onOneTimePermissionSessionTimeout(@NonNull String);
-    method @FlaggedApi(Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS) @BinderThread public void onOneTimePermissionSessionTimeout(@NonNull String, int);
+    method @FlaggedApi("android.permission.flags.device_aware_permission_apis") @BinderThread public void onOneTimePermissionSessionTimeout(@NonNull String, int);
     method @Deprecated @BinderThread public void onRestoreDelayedRuntimePermissionsBackup(@NonNull String, @NonNull android.os.UserHandle, @NonNull java.util.function.Consumer<java.lang.Boolean>);
     method @Deprecated @BinderThread public void onRestoreRuntimePermissionsBackup(@NonNull android.os.UserHandle, @NonNull java.io.InputStream, @NonNull Runnable);
     method @BinderThread public abstract void onRevokeRuntimePermission(@NonNull String, @NonNull String, @NonNull Runnable);
     method @BinderThread public abstract void onRevokeRuntimePermissions(@NonNull java.util.Map<java.lang.String,java.util.List<java.lang.String>>, boolean, int, @NonNull String, @NonNull java.util.function.Consumer<java.util.Map<java.lang.String,java.util.List<java.lang.String>>>);
     method @Deprecated @BinderThread public void onRevokeSelfPermissionsOnKill(@NonNull String, @NonNull java.util.List<java.lang.String>, @NonNull Runnable);
-    method @FlaggedApi(Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS) @BinderThread public void onRevokeSelfPermissionsOnKill(@NonNull String, @NonNull java.util.List<java.lang.String>, int, @NonNull Runnable);
+    method @FlaggedApi("android.permission.flags.device_aware_permission_apis") @BinderThread public void onRevokeSelfPermissionsOnKill(@NonNull String, @NonNull java.util.List<java.lang.String>, int, @NonNull Runnable);
     method @Deprecated @BinderThread public abstract void onSetRuntimePermissionGrantStateByDeviceAdmin(@NonNull String, @NonNull String, @NonNull String, int, @NonNull java.util.function.Consumer<java.lang.Boolean>);
     method @BinderThread public void onSetRuntimePermissionGrantStateByDeviceAdmin(@NonNull String, @NonNull android.permission.AdminPermissionControlParams, @NonNull java.util.function.Consumer<java.lang.Boolean>);
     method @BinderThread public void onStageAndApplyRuntimePermissionsBackup(@NonNull android.os.UserHandle, @NonNull java.io.InputStream, @NonNull Runnable);
@@ -16777,13 +16777,13 @@
     field public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_SEND_FAILED = 3; // 0x3
     field public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_SEND_SUCCESS = 2; // 0x2
     field public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_UNKNOWN = -1; // 0xffffffff
-    field @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_WAITING_TO_CONNECT = 8; // 0x8
-    field @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_MODEM_STATE_CONNECTED = 7; // 0x7
+    field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_WAITING_TO_CONNECT = 8; // 0x8
+    field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_MODEM_STATE_CONNECTED = 7; // 0x7
     field public static final int SATELLITE_MODEM_STATE_DATAGRAM_RETRYING = 3; // 0x3
     field public static final int SATELLITE_MODEM_STATE_DATAGRAM_TRANSFERRING = 2; // 0x2
     field public static final int SATELLITE_MODEM_STATE_IDLE = 0; // 0x0
     field public static final int SATELLITE_MODEM_STATE_LISTENING = 1; // 0x1
-    field @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_MODEM_STATE_NOT_CONNECTED = 6; // 0x6
+    field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_MODEM_STATE_NOT_CONNECTED = 6; // 0x6
     field public static final int SATELLITE_MODEM_STATE_OFF = 4; // 0x4
     field public static final int SATELLITE_MODEM_STATE_UNAVAILABLE = 5; // 0x5
     field public static final int SATELLITE_MODEM_STATE_UNKNOWN = -1; // 0xffffffff
diff --git a/core/java/android/companion/virtual/VirtualDevice.java b/core/java/android/companion/virtual/VirtualDevice.java
index 4692f92..ce883cd 100644
--- a/core/java/android/companion/virtual/VirtualDevice.java
+++ b/core/java/android/companion/virtual/VirtualDevice.java
@@ -44,6 +44,7 @@
     private final int mId;
     private final @Nullable String mPersistentId;
     private final @Nullable String mName;
+    private final @Nullable CharSequence mDisplayName;
 
     /**
      * Creates a new instance of {@link VirtualDevice}.
@@ -53,6 +54,18 @@
      */
     public VirtualDevice(@NonNull IVirtualDevice virtualDevice, int id,
             @Nullable String persistentId, @Nullable String name) {
+        this(virtualDevice, id, persistentId, name, null);
+    }
+
+    /**
+     * Creates a new instance of {@link VirtualDevice}. Only to be used by the
+     * VirtualDeviceManagerService.
+     *
+     * @hide
+     */
+    public VirtualDevice(@NonNull IVirtualDevice virtualDevice, int id,
+            @Nullable String persistentId, @Nullable String name,
+            @Nullable CharSequence displayName) {
         if (id <= Context.DEVICE_ID_DEFAULT) {
             throw new IllegalArgumentException("VirtualDevice ID must be greater than "
                     + Context.DEVICE_ID_DEFAULT);
@@ -61,6 +74,7 @@
         mId = id;
         mPersistentId = persistentId;
         mName = name;
+        mDisplayName = displayName;
     }
 
     private VirtualDevice(@NonNull Parcel parcel) {
@@ -68,6 +82,7 @@
         mId = parcel.readInt();
         mPersistentId = parcel.readString8();
         mName = parcel.readString8();
+        mDisplayName = parcel.readCharSequence();
     }
 
     /**
@@ -112,6 +127,15 @@
     }
 
     /**
+     * Returns the human-readable name of the virtual device, if defined, which is suitable to be
+     * shown in UI.
+     */
+    @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS)
+    public @Nullable CharSequence getDisplayName() {
+        return mDisplayName;
+    }
+
+    /**
      * Returns the IDs of all virtual displays that belong to this device, if any.
      *
      * <p>The actual {@link android.view.Display} objects can be obtained by passing the returned
@@ -156,6 +180,7 @@
         dest.writeInt(mId);
         dest.writeString8(mPersistentId);
         dest.writeString8(mName);
+        dest.writeCharSequence(mDisplayName);
     }
 
     @Override
@@ -165,6 +190,7 @@
                 + " mId=" + mId
                 + " mPersistentId=" + mPersistentId
                 + " mName=" + mName
+                + " mDisplayName=" + mDisplayName
                 + ")";
     }
 
diff --git a/core/java/android/companion/virtual/flags.aconfig b/core/java/android/companion/virtual/flags.aconfig
index 3e96c96..d0e13cd 100644
--- a/core/java/android/companion/virtual/flags.aconfig
+++ b/core/java/android/companion/virtual/flags.aconfig
@@ -34,3 +34,10 @@
     description: "Enable Virtual Camera"
     bug: "270352264"
 }
+
+flag {
+  name: "stream_permissions"
+  namespace: "virtual_devices"
+  description: "Enable streaming permission dialogs to Virtual Devices"
+  bug: "291737919"
+}
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index 4700720..4791a83 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -698,4 +698,34 @@
             return "AmbientLightSensorData(" + sensorName + ", " + sensorType + ")";
         }
     }
+
+    /**
+     * Associate a internal display to a {@link DisplayOffloader}.
+     *
+     * @param displayId the id of the internal display.
+     * @param displayOffloader the {@link DisplayOffloader} that controls offloading ops of internal
+     *                         display whose id is displayId.
+     * @return a {@link DisplayOffloadSession} associated with given displayId and displayOffloader.
+     */
+    public abstract DisplayOffloadSession registerDisplayOffloader(
+            int displayId, DisplayOffloader displayOffloader);
+
+    /** The callbacks that controls the entry & exit of display offloading. */
+    public interface DisplayOffloader {
+        boolean startOffload();
+
+        void stopOffload();
+    }
+
+    /** A session token that associates a internal display with a {@link DisplayOffloader}. */
+    public interface DisplayOffloadSession {
+        /** Provide the display state to use in place of state DOZE. */
+        void setDozeStateOverride(int displayState);
+        /** Returns the associated DisplayOffloader. */
+        DisplayOffloader getDisplayOffloader();
+        /** Returns whether displayoffload supports the given display state. */
+        static boolean isSupportedOffloadState(int displayState) {
+            return Display.isSuspendedState(displayState);
+        }
+    }
 }
diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
index 94d8516..6a82f6d 100644
--- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java
+++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
@@ -959,8 +959,8 @@
                 mKeyphraseMetadata = new KeyphraseMetadata(1, mText, fakeSupportedLocales,
                         AlwaysOnHotwordDetector.RECOGNITION_MODE_VOICE_TRIGGER);
             }
-            notifyStateChangedLocked();
         }
+        notifyStateChanged(availability);
     }
 
     /**
@@ -1370,8 +1370,8 @@
 
             mAvailability = STATE_INVALID;
             mIsAvailabilityOverriddenByTestApi = false;
-            notifyStateChangedLocked();
         }
+        notifyStateChanged(STATE_INVALID);
         super.destroy();
     }
 
@@ -1401,6 +1401,8 @@
      */
     // TODO(b/281608561): remove the enrollment flow from AlwaysOnHotwordDetector
     void onSoundModelsChanged() {
+        boolean notifyError = false;
+
         synchronized (mLock) {
             if (mAvailability == STATE_INVALID
                     || mAvailability == STATE_HARDWARE_UNAVAILABLE
@@ -1441,6 +1443,9 @@
                     // calling stopRecognition where there is no started session.
                     Log.w(TAG, "Failed to stop recognition after enrollment update: code="
                             + result);
+
+                    // Execute a refresh availability task - which should then notify of a change.
+                    new RefreshAvailabilityTask().execute();
                 } catch (Exception e) {
                     Slog.w(TAG, "Failed to stop recognition after enrollment update", e);
                     if (CompatChanges.isChangeEnabled(SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS)) {
@@ -1449,14 +1454,14 @@
                                         + Log.getStackTraceString(e),
                                 FailureSuggestedAction.RECREATE_DETECTOR));
                     } else {
-                        updateAndNotifyStateChangedLocked(STATE_ERROR);
+                        notifyError = true;
                     }
-                    return;
                 }
             }
+        }
 
-            // Execute a refresh availability task - which should then notify of a change.
-            new RefreshAvailabilityTask().execute();
+        if (notifyError) {
+            updateAndNotifyStateChanged(STATE_ERROR);
         }
     }
 
@@ -1572,10 +1577,11 @@
         }
     }
 
-    @GuardedBy("mLock")
-    private void updateAndNotifyStateChangedLocked(int availability) {
-        updateAvailabilityLocked(availability);
-        notifyStateChangedLocked();
+    private void updateAndNotifyStateChanged(int availability) {
+        synchronized (mLock) {
+            updateAvailabilityLocked(availability);
+        }
+        notifyStateChanged(availability);
     }
 
     @GuardedBy("mLock")
@@ -1589,17 +1595,17 @@
         }
     }
 
-    @GuardedBy("mLock")
-    private void notifyStateChangedLocked() {
+    private void notifyStateChanged(int newAvailability) {
         Message message = Message.obtain(mHandler, MSG_AVAILABILITY_CHANGED);
-        message.arg1 = mAvailability;
+        message.arg1 = newAvailability;
         message.sendToTarget();
     }
 
-    @GuardedBy("mLock")
     private void sendUnknownFailure(String failureMessage) {
-        // update but do not call onAvailabilityChanged callback for STATE_ERROR
-        updateAvailabilityLocked(STATE_ERROR);
+        synchronized (mLock) {
+            // update but do not call onAvailabilityChanged callback for STATE_ERROR
+            updateAvailabilityLocked(STATE_ERROR);
+        }
         Message.obtain(mHandler, MSG_DETECTION_UNKNOWN_FAILURE, failureMessage).sendToTarget();
     }
 
@@ -1802,19 +1808,17 @@
                             availability = STATE_KEYPHRASE_UNENROLLED;
                         }
                     }
-                    updateAndNotifyStateChangedLocked(availability);
                 }
+                updateAndNotifyStateChanged(availability);
             } catch (Exception e) {
                 // Any exception here not caught will crash the process because AsyncTask does not
                 // bubble up the exceptions to the client app, so we must propagate it to the app.
                 Slog.w(TAG, "Failed to refresh availability", e);
-                synchronized (mLock) {
-                    if (CompatChanges.isChangeEnabled(SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS)) {
-                        sendUnknownFailure(
-                                "Failed to refresh availability: " + Log.getStackTraceString(e));
-                    } else {
-                        updateAndNotifyStateChangedLocked(STATE_ERROR);
-                    }
+                if (CompatChanges.isChangeEnabled(SEND_ON_FAILURE_FOR_ASYNC_EXCEPTIONS)) {
+                    sendUnknownFailure(
+                            "Failed to refresh availability: " + Log.getStackTraceString(e));
+                } else {
+                    updateAndNotifyStateChanged(STATE_ERROR);
                 }
             }
 
diff --git a/core/java/android/text/ClientFlags.java b/core/java/android/text/ClientFlags.java
new file mode 100644
index 0000000..46fa501
--- /dev/null
+++ b/core/java/android/text/ClientFlags.java
@@ -0,0 +1,58 @@
+/*
+ * 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 android.text;
+
+import com.android.text.flags.Flags;
+
+/**
+ * An aconfig feature flags that can be accessible from application process without
+ * ContentProvider IPCs.
+ *
+ * When you add new flags, you have to add flag string to {@link TextFlags#TEXT_ACONFIGS_FLAGS}.
+ *
+ * @hide
+ */
+public class ClientFlags {
+
+    /**
+     * @see Flags#deprecateFontsXml()
+     */
+    public static boolean deprecateFontsXml() {
+        return TextFlags.isFeatureEnabled(Flags.FLAG_DEPRECATE_FONTS_XML);
+    }
+
+    /**
+     * @see Flags#noBreakNoHyphenationSpan()
+     */
+    public static boolean noBreakNoHyphenationSpan() {
+        return TextFlags.isFeatureEnabled(Flags.FLAG_NO_BREAK_NO_HYPHENATION_SPAN);
+    }
+
+    /**
+     * @see Flags#phraseStrictFallback()
+     */
+    public static boolean phraseStrictFallback() {
+        return TextFlags.isFeatureEnabled(Flags.FLAG_PHRASE_STRICT_FALLBACK);
+    }
+
+    /**
+     * @see Flags#useBoundsForWidth()
+     */
+    public static boolean useBoundsForWidth() {
+        return TextFlags.isFeatureEnabled(Flags.FLAG_USE_BOUNDS_FOR_WIDTH);
+    }
+}
diff --git a/core/java/android/text/TextFlags.java b/core/java/android/text/TextFlags.java
index 4be6a8d..536e3cc 100644
--- a/core/java/android/text/TextFlags.java
+++ b/core/java/android/text/TextFlags.java
@@ -16,6 +16,11 @@
 
 package android.text;
 
+import android.annotation.NonNull;
+import android.app.AppGlobals;
+
+import com.android.text.flags.Flags;
+
 /**
  * Flags in the "text" namespace.
  *
@@ -46,4 +51,28 @@
      */
     public static final boolean ENABLE_NEW_CONTEXT_MENU_DEFAULT = true;
 
+    /**
+     * List of text flags to be transferred to the application process.
+     */
+    public static final String[] TEXT_ACONFIGS_FLAGS = {
+            Flags.FLAG_DEPRECATE_FONTS_XML,
+            Flags.FLAG_NO_BREAK_NO_HYPHENATION_SPAN,
+            Flags.FLAG_PHRASE_STRICT_FALLBACK,
+            Flags.FLAG_USE_BOUNDS_FOR_WIDTH,
+    };
+
+    /**
+     * Get a key for the feature flag.
+     */
+    public static String getKeyForFlag(@NonNull String flag) {
+        return "text__" + flag;
+    }
+
+    /**
+     * Return true if the feature flag is enabled.
+     */
+    public static boolean isFeatureEnabled(@NonNull String flag) {
+        return AppGlobals.getIntCoreSetting(
+                getKeyForFlag(flag), 0 /* aconfig is false by default */) != 0;
+    }
 }
diff --git a/core/java/android/view/TextureView.java b/core/java/android/view/TextureView.java
index bdd0a9c..969b6a512 100644
--- a/core/java/android/view/TextureView.java
+++ b/core/java/android/view/TextureView.java
@@ -16,6 +16,7 @@
 
 package android.view;
 
+import android.annotation.FloatRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.compat.annotation.UnsupportedAppUsage;
@@ -30,8 +31,10 @@
 import android.graphics.TextureLayer;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
+import android.os.Trace;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.view.flags.Flags;
 
 /**
  * <p>A TextureView can be used to display a content stream, such as that
@@ -194,6 +197,9 @@
     private Canvas mCanvas;
     private int mSaveCount;
 
+    @FloatRange(from = 0.0) float mFrameRate;
+    @Surface.FrameRateCompatibility int mFrameRateCompatibility;
+
     private final Object[] mNativeWindowLock = new Object[0];
     // Set by native code, do not write!
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
@@ -465,6 +471,16 @@
             mLayer.setSurfaceTexture(mSurface);
             mSurface.setDefaultBufferSize(getWidth(), getHeight());
             mSurface.setOnFrameAvailableListener(mUpdateListener, mAttachInfo.mHandler);
+            if (Flags.toolkitSetFrameRate()) {
+                mSurface.setOnSetFrameRateListener(
+                        (surfaceTexture, frameRate, compatibility, strategy) -> {
+                            if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
+                                Trace.instant(Trace.TRACE_TAG_VIEW, "setFrameRate: " + frameRate);
+                            }
+                            mFrameRate = frameRate;
+                            mFrameRateCompatibility = compatibility;
+                        }, mAttachInfo.mHandler);
+            }
 
             if (mListener != null && createNewSurface) {
                 mListener.onSurfaceTextureAvailable(mSurface, getWidth(), getHeight());
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index a0d0656..2c41330 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -103,6 +103,7 @@
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.text.BoringLayout;
+import android.text.ClientFlags;
 import android.text.DynamicLayout;
 import android.text.Editable;
 import android.text.GetChars;
@@ -1634,7 +1635,7 @@
         }
 
         if (CompatChanges.isChangeEnabled(USE_BOUNDS_FOR_WIDTH)) {
-            mUseBoundsForWidth = false;  // TODO: Connect to the flag.
+            mUseBoundsForWidth = ClientFlags.useBoundsForWidth();
         } else {
             mUseBoundsForWidth = false;
         }
diff --git a/core/java/android/window/StartingWindowInfo.java b/core/java/android/window/StartingWindowInfo.java
index a88e394..451acbe 100644
--- a/core/java/android/window/StartingWindowInfo.java
+++ b/core/java/android/window/StartingWindowInfo.java
@@ -122,7 +122,7 @@
             TYPE_PARAMETER_PROCESS_RUNNING,
             TYPE_PARAMETER_ALLOW_TASK_SNAPSHOT,
             TYPE_PARAMETER_ACTIVITY_CREATED,
-            TYPE_PARAMETER_ALLOW_ICON,
+            TYPE_PARAMETER_USE_SOLID_COLOR_SPLASH_SCREEN,
             TYPE_PARAMETER_ALLOW_HANDLE_SOLID_COLOR_SCREEN,
             TYPE_PARAMETER_WINDOWLESS,
             TYPE_PARAMETER_LEGACY_SPLASH_SCREEN
@@ -143,7 +143,7 @@
     /** @hide */
     public static final int TYPE_PARAMETER_ACTIVITY_CREATED = 0x00000010;
     /** @hide */
-    public static final int TYPE_PARAMETER_ALLOW_ICON = 0x00000020;
+    public static final int TYPE_PARAMETER_USE_SOLID_COLOR_SPLASH_SCREEN = 0x00000020;
     /**
      * The parameter which indicates if the activity has finished drawing.
      * @hide
diff --git a/core/java/com/android/internal/display/BrightnessSynchronizer.java b/core/java/com/android/internal/display/BrightnessSynchronizer.java
index d503904..7a87c3a 100644
--- a/core/java/com/android/internal/display/BrightnessSynchronizer.java
+++ b/core/java/com/android/internal/display/BrightnessSynchronizer.java
@@ -16,9 +16,11 @@
 
 package com.android.internal.display;
 
+import android.annotation.SuppressLint;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.database.ContentObserver;
+import android.hardware.display.BrightnessInfo;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManager.DisplayListener;
 import android.net.Uri;
@@ -54,8 +56,7 @@
     private static final int MSG_RUN_UPDATE = 1;
 
     // The tolerance within which we consider brightness values approximately equal to eachother.
-    // This value is approximately 1/3 of the smallest possible brightness value.
-    public static final float EPSILON = 0.001f;
+    public static final float EPSILON = 0.0001f;
 
     private static int sBrightnessUpdateCount = 1;
 
@@ -284,6 +285,74 @@
     }
 
     /**
+     * Converts between the int brightness setting and the float brightness system. The int
+     * brightness setting is between 0-255 and matches the brightness slider - e.g. 128 is 50% on
+     * the slider. Accounts for special values such as OFF and invalid values. Accounts for
+     * brightness limits; the maximum value here represents the max value allowed on the slider.
+     */
+    @VisibleForTesting
+    @SuppressLint("AndroidFrameworkRequiresPermission")
+    public float brightnessIntSettingToFloat(int brightnessInt) {
+        if (brightnessInt == PowerManager.BRIGHTNESS_OFF) {
+            return PowerManager.BRIGHTNESS_OFF_FLOAT;
+        } else if (brightnessInt == PowerManager.BRIGHTNESS_INVALID) {
+            return PowerManager.BRIGHTNESS_INVALID_FLOAT;
+        } else {
+            final float minInt = PowerManager.BRIGHTNESS_OFF + 1;
+            final float maxInt = PowerManager.BRIGHTNESS_ON;
+
+            // Normalize to the range [0, 1]
+            float userPerceptionBrightness = MathUtils.norm(minInt, maxInt, brightnessInt);
+
+            // Convert from user-perception to linear scale
+            float linearBrightness = BrightnessUtils.convertGammaToLinear(userPerceptionBrightness);
+
+            // Interpolate to the range [0, currentlyAllowedMax]
+            final Display display = mContext.getDisplay();
+            if (display == null) {
+                return PowerManager.BRIGHTNESS_INVALID_FLOAT;
+            }
+            final BrightnessInfo info = display.getBrightnessInfo();
+            return MathUtils.lerp(info.brightnessMinimum, info.brightnessMaximum, linearBrightness);
+        }
+    }
+
+    /**
+     * Translates specified value from the float brightness system to the setting int brightness
+     * system. The value returned is between 0-255 and matches the brightness slider - e.g. 128 is
+     * 50% on the slider. Accounts for special values such as OFF and invalid values. Accounts for
+     * brightness limits; the maximum value here represents the max value currently allowed on
+     * the slider.
+     */
+    @VisibleForTesting
+    @SuppressLint("AndroidFrameworkRequiresPermission")
+    public int brightnessFloatToIntSetting(float brightnessFloat) {
+        if (floatEquals(brightnessFloat, PowerManager.BRIGHTNESS_OFF_FLOAT)) {
+            return PowerManager.BRIGHTNESS_OFF;
+        } else if (Float.isNaN(brightnessFloat)) {
+            return PowerManager.BRIGHTNESS_INVALID;
+        } else {
+            // Normalize to the range [0, 1]
+            final Display display = mContext.getDisplay();
+            if (display == null) {
+                return PowerManager.BRIGHTNESS_INVALID;
+            }
+            final BrightnessInfo info = display.getBrightnessInfo();
+            float linearBrightness =
+                    MathUtils.norm(info.brightnessMinimum, info.brightnessMaximum, brightnessFloat);
+
+            // Convert from linear to user-perception scale
+            float userPerceptionBrightness = BrightnessUtils.convertLinearToGamma(linearBrightness);
+
+            // Interpolate to the range [0, 255]
+            final float minInt = PowerManager.BRIGHTNESS_OFF + 1;
+            final float maxInt = PowerManager.BRIGHTNESS_ON;
+            float intBrightness = MathUtils.lerp(minInt, maxInt, userPerceptionBrightness);
+            return Math.round(intBrightness);
+        }
+    }
+
+    /**
      * Encapsulates a brightness change event and contains logic for synchronizing the appropriate
      * settings for the specified brightness change.
      */
@@ -421,14 +490,14 @@
             if (mSourceType == TYPE_INT) {
                 return (int) mBrightness;
             }
-            return brightnessFloatToInt(mBrightness);
+            return brightnessFloatToIntSetting(mBrightness);
         }
 
         private float getBrightnessAsFloat() {
             if (mSourceType == TYPE_FLOAT) {
                 return mBrightness;
             }
-            return brightnessIntToFloat((int) mBrightness);
+            return brightnessIntSettingToFloat((int) mBrightness);
         }
 
         private String toStringLabel(int type, float brightness) {
diff --git a/services/core/java/com/android/server/display/BrightnessUtils.java b/core/java/com/android/internal/display/BrightnessUtils.java
similarity index 96%
rename from services/core/java/com/android/server/display/BrightnessUtils.java
rename to core/java/com/android/internal/display/BrightnessUtils.java
index 84fa0cc..82b506b 100644
--- a/services/core/java/com/android/server/display/BrightnessUtils.java
+++ b/core/java/com/android/internal/display/BrightnessUtils.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.display;
+package com.android.internal.display;
 
 import android.util.MathUtils;
 
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index c6f5086..7eeac29 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -154,6 +154,7 @@
 
     void addQsTile(in ComponentName tile);
     void remQsTile(in ComponentName tile);
+    void setQsTiles(in String[] tiles);
     void clickQsTile(in ComponentName tile);
     void handleSystemKey(in KeyEvent key);
 
diff --git a/core/java/com/android/internal/widget/CallLayout.java b/core/java/com/android/internal/widget/CallLayout.java
index acb0e44..89f4659 100644
--- a/core/java/com/android/internal/widget/CallLayout.java
+++ b/core/java/com/android/internal/widget/CallLayout.java
@@ -49,6 +49,7 @@
     private CachingIconView mIcon;
     private CachingIconView mConversationIconBadgeBg;
     private TextView mConversationText;
+    private boolean mSetDataAsyncEnabled = false;
 
     public CallLayout(@NonNull Context context) {
         super(context);
@@ -83,7 +84,8 @@
         });
     }
 
-    private void updateCallLayout() {
+    @NonNull
+    private Icon getConversationIcon() {
         CharSequence callerName = "";
         String symbol = "";
         Icon icon = null;
@@ -98,8 +100,7 @@
         if (icon == null) {
             icon = mPeopleHelper.createAvatarSymbol(callerName, symbol, mLayoutColor);
         }
-        // TODO(b/179178086): crop/clip the icon to a circle?
-        mConversationIconView.setImageIcon(icon);
+        return icon;
     }
 
     @RemotableViewMethod
@@ -123,10 +124,38 @@
     /**
      * Set the notification extras so that this layout has access
      */
-    @RemotableViewMethod
+    @RemotableViewMethod(asyncImpl = "setDataAsync")
     public void setData(Bundle extras) {
-        setUser(extras.getParcelable(Notification.EXTRA_CALL_PERSON, android.app.Person.class));
-        updateCallLayout();
+        final Person person = getPerson(extras);
+        setUser(person);
+
+        final Icon icon = getConversationIcon();
+        mConversationIconView.setImageIcon(icon);
+    }
+
+
+    public void setSetDataAsyncEnabled(boolean setDataAsyncEnabled) {
+        mSetDataAsyncEnabled = setDataAsyncEnabled;
+    }
+
+    /**
+     * Async implementation for setData
+     */
+    public Runnable setDataAsync(Bundle extras) {
+        if (!mSetDataAsyncEnabled) {
+            return () -> setData(extras);
+        }
+
+        final Person person = getPerson(extras);
+        setUser(person);
+
+        final Icon conversationIcon = getConversationIcon();
+        return mConversationIconView.setImageIconAsync(conversationIcon);
+    }
+
+    @Nullable
+    private Person getPerson(Bundle extras) {
+        return extras.getParcelable(Notification.EXTRA_CALL_PERSON, Person.class);
     }
 
     private void setUser(Person user) {
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index 9ed4155..3795fc8 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -102,6 +102,7 @@
     static_libs: [
         "libnativehelper_lazy",
         "libziparchive_for_incfs",
+        "libguiflags",
     ],
 
     export_include_dirs: [
diff --git a/core/jni/android_graphics_SurfaceTexture.cpp b/core/jni/android_graphics_SurfaceTexture.cpp
index 21487ab..50832a5 100644
--- a/core/jni/android_graphics_SurfaceTexture.cpp
+++ b/core/jni/android_graphics_SurfaceTexture.cpp
@@ -17,27 +17,24 @@
 #undef LOG_TAG
 #define LOG_TAG "SurfaceTexture"
 
-#include <stdio.h>
-
 #include <EGL/egl.h>
 #include <EGL/eglext.h>
 #include <GLES2/gl2.h>
 #include <GLES2/gl2ext.h>
-
+#include <com_android_graphics_libgui_flags.h>
+#include <cutils/atomic.h>
 #include <gui/BufferQueue.h>
 #include <gui/Surface.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedLocalRef.h>
+#include <stdio.h>
 #include <surfacetexture/SurfaceTexture.h>
 #include <surfacetexture/surface_texture_platform.h>
-
-#include "core_jni_helpers.h"
-
-#include <cutils/atomic.h>
 #include <utils/Log.h>
 #include <utils/misc.h>
 
+#include "core_jni_helpers.h"
 #include "jni.h"
-#include <nativehelper/JNIHelp.h>
-#include <nativehelper/ScopedLocalRef.h>
 
 // ----------------------------------------------------------------------------
 
@@ -55,6 +52,7 @@
     jfieldID  producer;
     jfieldID  frameAvailableListener;
     jmethodID postEvent;
+    jmethodID postOnSetFrameRateEvent;
 };
 static fields_t fields;
 
@@ -139,61 +137,81 @@
 
 // ----------------------------------------------------------------------------
 
-class JNISurfaceTextureContext : public SurfaceTexture::FrameAvailableListener
-{
+class JNISurfaceTextureContextCommon {
 public:
-    JNISurfaceTextureContext(JNIEnv* env, jobject weakThiz, jclass clazz);
-    virtual ~JNISurfaceTextureContext();
-    virtual void onFrameAvailable(const BufferItem& item);
+    JNISurfaceTextureContextCommon(JNIEnv* env, jobject weakThiz, jclass clazz)
+          : mWeakThiz(env->NewGlobalRef(weakThiz)), mClazz((jclass)env->NewGlobalRef(clazz)) {}
 
-private:
-    static JNIEnv* getJNIEnv();
+    virtual ~JNISurfaceTextureContextCommon() {
+        JNIEnv* env = getJNIEnv();
+        if (env != NULL) {
+            env->DeleteGlobalRef(mWeakThiz);
+            env->DeleteGlobalRef(mClazz);
+        } else {
+            ALOGW("leaking JNI object references");
+        }
+    }
+
+    void onFrameAvailable(const BufferItem& item) {
+        JNIEnv* env = getJNIEnv();
+        if (env != NULL) {
+            env->CallStaticVoidMethod(mClazz, fields.postEvent, mWeakThiz);
+        } else {
+            ALOGW("onFrameAvailable event will not posted");
+        }
+    }
+
+protected:
+    static JNIEnv* getJNIEnv() {
+        JNIEnv* env = AndroidRuntime::getJNIEnv();
+        if (env == NULL) {
+            JavaVMAttachArgs args = {JNI_VERSION_1_4, "JNISurfaceTextureContext", NULL};
+            JavaVM* vm = AndroidRuntime::getJavaVM();
+            int result = vm->AttachCurrentThreadAsDaemon(&env, (void*)&args);
+            if (result != JNI_OK) {
+                ALOGE("thread attach failed: %#x", result);
+                return NULL;
+            }
+        }
+        return env;
+    }
 
     jobject mWeakThiz;
     jclass mClazz;
 };
 
-JNISurfaceTextureContext::JNISurfaceTextureContext(JNIEnv* env,
-        jobject weakThiz, jclass clazz) :
-    mWeakThiz(env->NewGlobalRef(weakThiz)),
-    mClazz((jclass)env->NewGlobalRef(clazz))
-{}
+class JNISurfaceTextureContextFrameAvailableListener
+      : public JNISurfaceTextureContextCommon,
+        public SurfaceTexture::FrameAvailableListener {
+public:
+    JNISurfaceTextureContextFrameAvailableListener(JNIEnv* env, jobject weakThiz, jclass clazz)
+          : JNISurfaceTextureContextCommon(env, weakThiz, clazz) {}
+    void onFrameAvailable(const BufferItem& item) override {
+        JNISurfaceTextureContextCommon::onFrameAvailable(item);
+    }
+};
 
-JNIEnv* JNISurfaceTextureContext::getJNIEnv() {
-    JNIEnv* env = AndroidRuntime::getJNIEnv();
-    if (env == NULL) {
-        JavaVMAttachArgs args = {
-            JNI_VERSION_1_4, "JNISurfaceTextureContext", NULL };
-        JavaVM* vm = AndroidRuntime::getJavaVM();
-        int result = vm->AttachCurrentThreadAsDaemon(&env, (void*)&args);
-        if (result != JNI_OK) {
-            ALOGE("thread attach failed: %#x", result);
-            return NULL;
+class JNISurfaceTextureContextListener : public JNISurfaceTextureContextCommon,
+                                         public SurfaceTexture::SurfaceTextureListener {
+public:
+    JNISurfaceTextureContextListener(JNIEnv* env, jobject weakThiz, jclass clazz)
+          : JNISurfaceTextureContextCommon(env, weakThiz, clazz) {}
+
+    void onFrameAvailable(const BufferItem& item) override {
+        JNISurfaceTextureContextCommon::onFrameAvailable(item);
+    }
+
+    void onSetFrameRate(float frameRate, int8_t compatibility,
+                        int8_t changeFrameRateStrategy) override {
+        JNIEnv* env = getJNIEnv();
+        if (env != NULL) {
+            env->CallStaticVoidMethod(mClazz, fields.postOnSetFrameRateEvent, mWeakThiz, frameRate,
+                                      compatibility, changeFrameRateStrategy);
+        } else {
+            ALOGW("onSetFrameRate event will not posted");
         }
     }
-    return env;
-}
-
-JNISurfaceTextureContext::~JNISurfaceTextureContext()
-{
-    JNIEnv* env = getJNIEnv();
-    if (env != NULL) {
-        env->DeleteGlobalRef(mWeakThiz);
-        env->DeleteGlobalRef(mClazz);
-    } else {
-        ALOGW("leaking JNI object references");
-    }
-}
-
-void JNISurfaceTextureContext::onFrameAvailable(const BufferItem& /* item */)
-{
-    JNIEnv* env = getJNIEnv();
-    if (env != NULL) {
-        env->CallStaticVoidMethod(mClazz, fields.postEvent, mWeakThiz);
-    } else {
-        ALOGW("onFrameAvailable event will not posted");
-    }
-}
+};
 
 // ----------------------------------------------------------------------------
 
@@ -229,6 +247,13 @@
     if (fields.postEvent == NULL) {
         ALOGE("can't find android/graphics/SurfaceTexture.postEventFromNative");
     }
+
+    fields.postOnSetFrameRateEvent =
+            env->GetStaticMethodID(clazz, "postOnSetFrameRateEventFromNative",
+                                   "(Ljava/lang/ref/WeakReference;FII)V");
+    if (fields.postOnSetFrameRateEvent == NULL) {
+        ALOGE("can't find android/graphics/SurfaceTexture.postOnSetFrameRateEventFromNative");
+    }
 }
 
 static void SurfaceTexture_init(JNIEnv* env, jobject thiz, jboolean isDetached,
@@ -274,17 +299,27 @@
         return;
     }
 
-    sp<JNISurfaceTextureContext> ctx(new JNISurfaceTextureContext(env, weakThiz,
-            clazz));
-    surfaceTexture->setFrameAvailableListener(ctx);
-    SurfaceTexture_setFrameAvailableListener(env, thiz, ctx);
+    if (com::android::graphics::libgui::flags::bq_setframerate()) {
+        sp<JNISurfaceTextureContextListener> ctx(
+                new JNISurfaceTextureContextListener(env, weakThiz, clazz));
+        surfaceTexture->setSurfaceTextureListener(ctx);
+    } else {
+        sp<JNISurfaceTextureContextFrameAvailableListener> ctx(
+                new JNISurfaceTextureContextFrameAvailableListener(env, weakThiz, clazz));
+        surfaceTexture->setFrameAvailableListener(ctx);
+        SurfaceTexture_setFrameAvailableListener(env, thiz, ctx);
+    }
 }
 
 static void SurfaceTexture_finalize(JNIEnv* env, jobject thiz)
 {
     sp<SurfaceTexture> surfaceTexture(SurfaceTexture_getSurfaceTexture(env, thiz));
-    surfaceTexture->setFrameAvailableListener(0);
-    SurfaceTexture_setFrameAvailableListener(env, thiz, 0);
+    if (com::android::graphics::libgui::flags::bq_setframerate()) {
+        surfaceTexture->setSurfaceTextureListener(0);
+    } else {
+        surfaceTexture->setFrameAvailableListener(0);
+        SurfaceTexture_setFrameAvailableListener(env, thiz, 0);
+    }
     SurfaceTexture_setSurfaceTexture(env, thiz, 0);
     SurfaceTexture_setProducer(env, thiz, 0);
 }
diff --git a/graphics/java/android/graphics/SurfaceTexture.java b/graphics/java/android/graphics/SurfaceTexture.java
index dfe5012..dd82fed 100644
--- a/graphics/java/android/graphics/SurfaceTexture.java
+++ b/graphics/java/android/graphics/SurfaceTexture.java
@@ -16,6 +16,7 @@
 
 package android.graphics;
 
+import android.annotation.FloatRange;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.compat.annotation.UnsupportedAppUsage;
@@ -24,8 +25,10 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
+import android.os.Trace;
 import android.view.Surface;
 import android.view.TextureView;
+import android.view.flags.Flags;
 
 import java.lang.ref.WeakReference;
 
@@ -79,6 +82,7 @@
     private final Looper mCreatorLooper;
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     private Handler mOnFrameAvailableHandler;
+    private Handler mOnSetFrameRateHandler;
 
     /**
      * These fields are used by native code, do not access or modify.
@@ -100,6 +104,21 @@
     }
 
     /**
+     * Callback interface for being notified that a producer set a frame rate
+     * @hide
+     */
+    public interface OnSetFrameRateListener {
+        /**
+         * Called when the producer sets a frame rate
+         * @hide
+         */
+        void onSetFrameRate(SurfaceTexture surfaceTexture,
+                            @FloatRange(from = 0.0) float frameRate,
+                                 @Surface.FrameRateCompatibility int compatibility,
+                                 @Surface.ChangeFrameRateStrategy int changeFrameRateStrategy);
+    }
+
+    /**
      * Exception thrown when a SurfaceTexture couldn't be created or resized.
      *
      * @deprecated No longer thrown. {@link android.view.Surface.OutOfResourcesException}
@@ -224,6 +243,48 @@
         }
     }
 
+    private static class SetFrameRateArgs {
+        SetFrameRateArgs(@FloatRange(from = 0.0) float frameRate,
+                                @Surface.FrameRateCompatibility int compatibility,
+                   @Surface.ChangeFrameRateStrategy int changeFrameRateStrategy) {
+            this.mFrameRate = frameRate;
+            this.mCompatibility = compatibility;
+            this.mChangeFrameRateStrategy = changeFrameRateStrategy;
+        }
+        final float mFrameRate;
+        final int mCompatibility;
+        final int mChangeFrameRateStrategy;
+    }
+
+    /**
+     * Register a callback to be invoked when the producer sets a frame rate using
+     * Surface.setFrameRate.
+     * @hide
+     */
+    public void setOnSetFrameRateListener(@Nullable final OnSetFrameRateListener listener,
+                                            @Nullable Handler handler) {
+        if (listener != null) {
+            Looper looper = handler != null ? handler.getLooper() :
+                    mCreatorLooper != null ? mCreatorLooper : Looper.getMainLooper();
+            mOnSetFrameRateHandler = new Handler(looper, null, true /*async*/) {
+                @Override
+                public void handleMessage(Message msg) {
+                    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "onSetFrameRateHandler");
+                    try {
+                        SetFrameRateArgs args = (SetFrameRateArgs) msg.obj;
+                        listener.onSetFrameRate(SurfaceTexture.this,
+                                args.mFrameRate, args.mCompatibility,
+                                args.mChangeFrameRateStrategy);
+                    } finally {
+                        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+                    }
+                }
+            };
+        } else {
+            mOnSetFrameRateHandler = null;
+        }
+    }
+
     /**
      * Set the default size of the image buffers.  The image producer may override the buffer size,
      * in which case the producer-set buffer size will be used, not the default size set by this
@@ -418,6 +479,35 @@
     }
 
     /**
+     * This method is invoked from native code only.
+     * @hide
+     */
+    @SuppressWarnings({"UnusedDeclaration"})
+    private static void postOnSetFrameRateEventFromNative(WeakReference<SurfaceTexture> weakSelf,
+            @FloatRange(from = 0.0) float frameRate,
+            @Surface.FrameRateCompatibility int compatibility,
+            @Surface.ChangeFrameRateStrategy int changeFrameRateStrategy) {
+        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "postOnSetFrameRateEventFromNative");
+        try {
+            if (Flags.toolkitSetFrameRate()) {
+                SurfaceTexture st = weakSelf.get();
+                if (st != null) {
+                    Handler handler = st.mOnSetFrameRateHandler;
+                    if (handler != null) {
+                        Message msg = new Message();
+                        msg.obj = new SetFrameRateArgs(frameRate, compatibility,
+                                changeFrameRateStrategy);
+                        handler.sendMessage(msg);
+                    }
+                }
+            }
+        } finally {
+            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+        }
+
+    }
+
+    /**
      * Returns {@code true} if the SurfaceTexture is single-buffered.
      * @hide
      */
diff --git a/keystore/java/android/security/keystore/KeyGenParameterSpec.java b/keystore/java/android/security/keystore/KeyGenParameterSpec.java
index 3956241..96c257b 100644
--- a/keystore/java/android/security/keystore/KeyGenParameterSpec.java
+++ b/keystore/java/android/security/keystore/KeyGenParameterSpec.java
@@ -16,6 +16,7 @@
 
 package android.security.keystore;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -34,7 +35,10 @@
 import java.security.Signature;
 import java.security.cert.Certificate;
 import java.security.spec.AlgorithmParameterSpec;
+import java.util.Collections;
 import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
 
 import javax.crypto.Cipher;
 import javax.crypto.KeyGenerator;
@@ -300,6 +304,7 @@
     private final Date mKeyValidityForConsumptionEnd;
     private final @KeyProperties.PurposeEnum int mPurposes;
     private final @KeyProperties.DigestEnum String[] mDigests;
+    private final @NonNull @KeyProperties.DigestEnum Set<String> mMgf1Digests;
     private final @KeyProperties.EncryptionPaddingEnum String[] mEncryptionPaddings;
     private final @KeyProperties.SignaturePaddingEnum String[] mSignaturePaddings;
     private final @KeyProperties.BlockModeEnum String[] mBlockModes;
@@ -345,6 +350,7 @@
             Date keyValidityForConsumptionEnd,
             @KeyProperties.PurposeEnum int purposes,
             @KeyProperties.DigestEnum String[] digests,
+            @KeyProperties.DigestEnum Set<String> mgf1Digests,
             @KeyProperties.EncryptionPaddingEnum String[] encryptionPaddings,
             @KeyProperties.SignaturePaddingEnum String[] signaturePaddings,
             @KeyProperties.BlockModeEnum String[] blockModes,
@@ -404,6 +410,9 @@
         mKeyValidityForConsumptionEnd = Utils.cloneIfNotNull(keyValidityForConsumptionEnd);
         mPurposes = purposes;
         mDigests = ArrayUtils.cloneIfNotEmpty(digests);
+        // No need to copy the input parameter because the Builder class passes in an immutable
+        // collection.
+        mMgf1Digests = mgf1Digests != null ? mgf1Digests : Collections.emptySet();
         mEncryptionPaddings =
                 ArrayUtils.cloneIfNotEmpty(ArrayUtils.nullToEmpty(encryptionPaddings));
         mSignaturePaddings = ArrayUtils.cloneIfNotEmpty(ArrayUtils.nullToEmpty(signaturePaddings));
@@ -566,7 +575,7 @@
 
     /**
      * Returns the set of digest algorithms (e.g., {@code SHA-256}, {@code SHA-384} with which the
-     * key can be used or {@code null} if not specified.
+     * key can be used.
      *
      * <p>See {@link KeyProperties}.{@code DIGEST} constants.
      *
@@ -594,6 +603,40 @@
     }
 
     /**
+     * Returns the set of digests that can be used by the MGF1 mask generation function
+     * (e.g., {@code SHA-256}, {@code SHA-384}) with the key. Useful with the {@code RSA-OAEP}
+     * scheme.
+     * If not explicitly specified during key generation, the default {@code SHA-1} digest is
+     * used and may be specified when using the key.
+     *
+     * <p>See {@link KeyProperties}.{@code DIGEST} constants.
+     *
+     * @throws IllegalStateException if this set has not been specified.
+     *
+     * @see #isMgf1DigestsSpecified()
+     */
+    @NonNull
+    @FlaggedApi("MGF1_DIGEST_SETTER")
+    public @KeyProperties.DigestEnum Set<String> getMgf1Digests() {
+        if (mMgf1Digests.isEmpty()) {
+            throw new IllegalStateException("Mask generation function (MGF) not specified");
+        }
+        return new HashSet(mMgf1Digests);
+    }
+
+    /**
+     * Returns {@code true} if the set of digests for the MGF1 mask generation function,
+     * with which the key can be used, has been specified. Useful with the {@code RSA-OAEP} scheme.
+     *
+     * @see #getMgf1Digests()
+     */
+    @NonNull
+    @FlaggedApi("MGF1_DIGEST_SETTER")
+    public boolean isMgf1DigestsSpecified() {
+        return !mMgf1Digests.isEmpty();
+    }
+
+    /**
      * Returns the set of padding schemes (e.g., {@code PKCS7Padding}, {@code OEAPPadding},
      * {@code PKCS1Padding}, {@code NoPadding}) with which the key can be used when
      * encrypting/decrypting. Attempts to use the key with any other padding scheme will be
@@ -913,6 +956,8 @@
         private Date mKeyValidityForOriginationEnd;
         private Date mKeyValidityForConsumptionEnd;
         private @KeyProperties.DigestEnum String[] mDigests;
+        private @NonNull @KeyProperties.DigestEnum Set<String> mMgf1Digests =
+                Collections.emptySet();
         private @KeyProperties.EncryptionPaddingEnum String[] mEncryptionPaddings;
         private @KeyProperties.SignaturePaddingEnum String[] mSignaturePaddings;
         private @KeyProperties.BlockModeEnum String[] mBlockModes;
@@ -983,6 +1028,9 @@
             if (sourceSpec.isDigestsSpecified()) {
                 mDigests = sourceSpec.getDigests();
             }
+            if (sourceSpec.isMgf1DigestsSpecified()) {
+                mMgf1Digests = sourceSpec.getMgf1Digests();
+            }
             mEncryptionPaddings = sourceSpec.getEncryptionPaddings();
             mSignaturePaddings = sourceSpec.getSignaturePaddings();
             mBlockModes = sourceSpec.getBlockModes();
@@ -1230,6 +1278,30 @@
         }
 
         /**
+         * Sets the set of hash functions (e.g., {@code SHA-256}, {@code SHA-384}) which could be
+         * used by the mask generation function MGF1 (which is used for certain operations with
+         * the key). Attempts to use the key with any other digest for the mask generation
+         * function will be rejected.
+         *
+         * <p>This can only be specified for signing/verification keys and RSA encryption/decryption
+         * keys used with RSA OAEP padding scheme because these operations involve a mask generation
+         * function (MGF1) with a digest.
+         * The default digest for MGF1 is {@code SHA-1}, which will be specified during key creation
+         * time if no digests have been explicitly provided.
+         * When using the key, the caller may not specify any digests that were not provided during
+         * key creation time. The caller may specify the default digest, {@code SHA-1}, if no
+         * digests were explicitly provided during key creation (but it is not necessary to do so).
+         *
+         * <p>See {@link KeyProperties}.{@code DIGEST} constants.
+         */
+        @NonNull
+        @FlaggedApi("MGF1_DIGEST_SETTER")
+        public Builder setMgf1Digests(@Nullable @KeyProperties.DigestEnum String... mgf1Digests) {
+            mMgf1Digests = Set.of(mgf1Digests);
+            return this;
+        }
+
+        /**
          * Sets the set of padding schemes (e.g., {@code PKCS7Padding}, {@code OAEPPadding},
          * {@code PKCS1Padding}, {@code NoPadding}) with which the key can be used when
          * encrypting/decrypting. Attempts to use the key with any other padding scheme will be
@@ -1782,6 +1854,7 @@
                     mKeyValidityForConsumptionEnd,
                     mPurposes,
                     mDigests,
+                    mMgf1Digests,
                     mEncryptionPaddings,
                     mSignaturePaddings,
                     mBlockModes,
diff --git a/keystore/java/android/security/keystore/KeyProtection.java b/keystore/java/android/security/keystore/KeyProtection.java
index 5ab21bc..c1e3bab 100644
--- a/keystore/java/android/security/keystore/KeyProtection.java
+++ b/keystore/java/android/security/keystore/KeyProtection.java
@@ -16,6 +16,7 @@
 
 package android.security.keystore;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -30,7 +31,10 @@
 import java.security.KeyStore.ProtectionParameter;
 import java.security.Signature;
 import java.security.cert.Certificate;
+import java.util.Collections;
 import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
 
 import javax.crypto.Cipher;
 import javax.crypto.Mac;
@@ -223,6 +227,7 @@
     private final @KeyProperties.EncryptionPaddingEnum String[] mEncryptionPaddings;
     private final @KeyProperties.SignaturePaddingEnum String[] mSignaturePaddings;
     private final @KeyProperties.DigestEnum String[] mDigests;
+    private final @NonNull @KeyProperties.DigestEnum Set<String> mMgf1Digests;
     private final @KeyProperties.BlockModeEnum String[] mBlockModes;
     private final boolean mRandomizedEncryptionRequired;
     private final boolean mUserAuthenticationRequired;
@@ -247,6 +252,7 @@
             @KeyProperties.EncryptionPaddingEnum String[] encryptionPaddings,
             @KeyProperties.SignaturePaddingEnum String[] signaturePaddings,
             @KeyProperties.DigestEnum String[] digests,
+            @KeyProperties.DigestEnum Set<String> mgf1Digests,
             @KeyProperties.BlockModeEnum String[] blockModes,
             boolean randomizedEncryptionRequired,
             boolean userAuthenticationRequired,
@@ -271,6 +277,7 @@
         mSignaturePaddings =
                 ArrayUtils.cloneIfNotEmpty(ArrayUtils.nullToEmpty(signaturePaddings));
         mDigests = ArrayUtils.cloneIfNotEmpty(digests);
+        mMgf1Digests = mgf1Digests;
         mBlockModes = ArrayUtils.cloneIfNotEmpty(ArrayUtils.nullToEmpty(blockModes));
         mRandomizedEncryptionRequired = randomizedEncryptionRequired;
         mUserAuthenticationRequired = userAuthenticationRequired;
@@ -381,6 +388,40 @@
     }
 
     /**
+     * Returns the set of digests that can be used by the MGF1 mask generation function
+     * (e.g., {@code SHA-256}, {@code SHA-384}) with the key. Useful with the {@code RSA-OAEP}
+     * scheme.
+     * If not explicitly specified  during key generation, the default {@code SHA-1} digest is
+     * used and may be specified.
+     *
+     * <p>See {@link KeyProperties}.{@code DIGEST} constants.
+     *
+     * @throws IllegalStateException if this set has not been specified.
+     *
+     * @see #isMgf1DigestsSpecified()
+     */
+    @NonNull
+    @FlaggedApi("MGF1_DIGEST_SETTER")
+    public @KeyProperties.DigestEnum Set<String> getMgf1Digests() {
+        if (mMgf1Digests.isEmpty()) {
+            throw new IllegalStateException("Mask generation function (MGF) not specified");
+        }
+        return new HashSet(mMgf1Digests);
+    }
+
+    /**
+     * Returns {@code true} if the set of digests for the MGF1 mask generation function,
+     * with which the key can be used, has been specified. Useful with the {@code RSA-OAEP} scheme.
+     *
+     * @see #getMgf1Digests()
+     */
+    @NonNull
+    @FlaggedApi("MGF1_DIGEST_SETTER")
+    public boolean isMgf1DigestsSpecified() {
+        return !mMgf1Digests.isEmpty();
+    }
+
+    /**
      * Gets the set of block modes (e.g., {@code GCM}, {@code CBC}) with which the key can be used
      * when encrypting/decrypting. Attempts to use the key with any other block modes will be
      * rejected.
@@ -588,6 +629,8 @@
         private @KeyProperties.EncryptionPaddingEnum String[] mEncryptionPaddings;
         private @KeyProperties.SignaturePaddingEnum String[] mSignaturePaddings;
         private @KeyProperties.DigestEnum String[] mDigests;
+        private @NonNull @KeyProperties.DigestEnum Set<String> mMgf1Digests =
+                Collections.emptySet();
         private @KeyProperties.BlockModeEnum String[] mBlockModes;
         private boolean mRandomizedEncryptionRequired = true;
         private boolean mUserAuthenticationRequired;
@@ -739,6 +782,30 @@
         }
 
         /**
+         * Sets the set of hash functions (e.g., {@code SHA-256}, {@code SHA-384}) which could be
+         * used by the mask generation function MGF1 (which is used for certain operations with
+         * the key). Attempts to use the key with any other digest for the mask generation
+         * function will be rejected.
+         *
+         * <p>This can only be specified for signing/verification keys and RSA encryption/decryption
+         * keys used with RSA OAEP padding scheme because these operations involve a mask generation
+         * function (MGF1) with a digest.
+         * The default digest for MGF1 is {@code SHA-1}, which will be specified during key import
+         * time if no digests have been explicitly provided.
+         * When using the key, the caller may not specify any digests that were not provided during
+         * key import time. The caller may specify the default digest, {@code SHA-1}, if no
+         * digests were explicitly provided during key import (but it is not necessary to do so).
+         *
+         * <p>See {@link KeyProperties}.{@code DIGEST} constants.
+         */
+        @NonNull
+        @FlaggedApi("MGF1_DIGEST_SETTER")
+        public Builder setMgf1Digests(@Nullable @KeyProperties.DigestEnum String... mgf1Digests) {
+            mMgf1Digests = Set.of(mgf1Digests);
+            return this;
+        }
+
+        /**
          * Sets the set of block modes (e.g., {@code GCM}, {@code CBC}) with which the key can be
          * used when encrypting/decrypting. Attempts to use the key with any other block modes will
          * be rejected.
@@ -1141,6 +1208,7 @@
                     mEncryptionPaddings,
                     mSignaturePaddings,
                     mDigests,
+                    mMgf1Digests,
                     mBlockModes,
                     mRandomizedEncryptionRequired,
                     mUserAuthenticationRequired,
diff --git a/keystore/java/android/security/keystore/ParcelableKeyGenParameterSpec.java b/keystore/java/android/security/keystore/ParcelableKeyGenParameterSpec.java
index 9356eb8..ceba04e 100644
--- a/keystore/java/android/security/keystore/ParcelableKeyGenParameterSpec.java
+++ b/keystore/java/android/security/keystore/ParcelableKeyGenParameterSpec.java
@@ -23,7 +23,11 @@
 import java.security.spec.AlgorithmParameterSpec;
 import java.security.spec.ECGenParameterSpec;
 import java.security.spec.RSAKeyGenParameterSpec;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Date;
+import java.util.List;
+import java.util.Set;
 
 import javax.security.auth.x500.X500Principal;
 
@@ -91,6 +95,11 @@
         } else {
             out.writeStringArray(null);
         }
+        if (mSpec.isMgf1DigestsSpecified()) {
+            out.writeStringList(List.copyOf(mSpec.getMgf1Digests()));
+        } else {
+            out.writeStringList(null);
+        }
         out.writeStringArray(mSpec.getEncryptionPaddings());
         out.writeStringArray(mSpec.getSignaturePaddings());
         out.writeStringArray(mSpec.getBlockModes());
@@ -153,6 +162,7 @@
         final Date keyValidityForOriginationEnd = readDateOrNull(in);
         final Date keyValidityForConsumptionEnd = readDateOrNull(in);
         final String[] digests = in.createStringArray();
+        final ArrayList<String> mgf1Digests = in.createStringArrayList();
         final String[] encryptionPaddings = in.createStringArray();
         final String[] signaturePaddings = in.createStringArray();
         final String[] blockModes = in.createStringArray();
@@ -191,6 +201,7 @@
                 keyValidityForConsumptionEnd,
                 purposes,
                 digests,
+                mgf1Digests != null ? Set.copyOf(mgf1Digests) : Collections.emptySet(),
                 encryptionPaddings,
                 signaturePaddings,
                 blockModes,
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreCipherSpiBase.java b/keystore/java/android/security/keystore2/AndroidKeyStoreCipherSpiBase.java
index 9ac0f6d..101a10e 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreCipherSpiBase.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreCipherSpiBase.java
@@ -24,6 +24,7 @@
 import android.security.KeyStoreException;
 import android.security.KeyStoreOperation;
 import android.security.keymaster.KeymasterDefs;
+import android.security.keystore.KeyProperties;
 import android.security.keystore.KeyStoreCryptoOperation;
 import android.system.keystore2.Authorization;
 
@@ -71,7 +72,7 @@
  */
 abstract class AndroidKeyStoreCipherSpiBase extends CipherSpi implements KeyStoreCryptoOperation {
     private static final String TAG = "AndroidKeyStoreCipherSpiBase";
-    public static final String DEFAULT_MGF1_DIGEST = "SHA-1";
+    public static final String DEFAULT_MGF1_DIGEST = KeyProperties.DIGEST_SHA1;
 
     // Fields below are populated by Cipher.init and KeyStore.begin and should be preserved after
     // doFinal finishes.
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
index 1398da3..ed4b485 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
@@ -188,6 +188,7 @@
     private int[] mKeymasterEncryptionPaddings;
     private int[] mKeymasterSignaturePaddings;
     private int[] mKeymasterDigests;
+    private int[] mKeymasterMgf1Digests;
 
     private Long mRSAPublicExponent;
 
@@ -323,6 +324,21 @@
                 } else {
                     mKeymasterDigests = EmptyArray.INT;
                 }
+                if (spec.isMgf1DigestsSpecified()) {
+                    // User-specified digests: Add all of them and do _not_ add the SHA-1
+                    // digest by default (stick to what the user provided).
+                    Set<String> mgfDigests = spec.getMgf1Digests();
+                    mKeymasterMgf1Digests = new int[mgfDigests.size()];
+                    int offset = 0;
+                    for (String digest : mgfDigests) {
+                        mKeymasterMgf1Digests[offset] = KeyProperties.Digest.toKeymaster(digest);
+                        offset++;
+                    }
+                } else {
+                    // No user-specified digests: Add the SHA-1 default.
+                    mKeymasterMgf1Digests = new int[]{
+                            KeyProperties.Digest.toKeymaster(DEFAULT_MGF1_DIGEST)};
+                }
 
                 // Check that user authentication related parameters are acceptable. This method
                 // will throw an IllegalStateException if there are issues (e.g., secure lock screen
@@ -544,6 +560,7 @@
         mKeymasterEncryptionPaddings = null;
         mKeymasterSignaturePaddings = null;
         mKeymasterDigests = null;
+        mKeymasterMgf1Digests = null;
         mKeySizeBits = 0;
         mSpec = null;
         mRSAPublicExponent = null;
@@ -831,24 +848,11 @@
                     KeymasterDefs.KM_TAG_PADDING, padding
             ));
             if (padding == KeymasterDefs.KM_PAD_RSA_OAEP) {
-                final boolean[] hasDefaultMgf1DigestBeenAdded = {false};
-                ArrayUtils.forEach(mKeymasterDigests, (digest) -> {
+                ArrayUtils.forEach(mKeymasterMgf1Digests, (mgf1Digest) -> {
                     params.add(KeyStore2ParameterUtils.makeEnum(
-                            KeymasterDefs.KM_TAG_RSA_OAEP_MGF_DIGEST, digest
+                            KeymasterDefs.KM_TAG_RSA_OAEP_MGF_DIGEST, mgf1Digest
                     ));
-                    hasDefaultMgf1DigestBeenAdded[0] |=
-                            digest.equals(KeyProperties.Digest.toKeymaster(DEFAULT_MGF1_DIGEST));
                 });
-                /* Because of default MGF1 digest is SHA-1. It has to be added in Key
-                 * characteristics. Otherwise, crypto operations will fail with Incompatible
-                 * MGF1 digest.
-                 */
-                if (!hasDefaultMgf1DigestBeenAdded[0]) {
-                    params.add(KeyStore2ParameterUtils.makeEnum(
-                            KeymasterDefs.KM_TAG_RSA_OAEP_MGF_DIGEST,
-                            KeyProperties.Digest.toKeymaster(DEFAULT_MGF1_DIGEST)
-                    ));
-                }
             }
         });
         ArrayUtils.forEach(mKeymasterSignaturePaddings, (padding) -> {
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java
index 273dff1..ddbd93e 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java
@@ -526,25 +526,22 @@
                         padding
                 ));
                 if (padding == KeymasterDefs.KM_PAD_RSA_OAEP) {
-                    if (spec.isDigestsSpecified()) {
-                        boolean hasDefaultMgf1DigestBeenAdded = false;
-                        for (String digest : spec.getDigests()) {
+                    if (spec.isMgf1DigestsSpecified()) {
+                        for (String mgf1Digest : spec.getMgf1Digests()) {
                             importArgs.add(KeyStore2ParameterUtils.makeEnum(
                                     KeymasterDefs.KM_TAG_RSA_OAEP_MGF_DIGEST,
-                                    KeyProperties.Digest.toKeymaster(digest)
+                                    KeyProperties.Digest.toKeymaster(mgf1Digest)
                             ));
-                            hasDefaultMgf1DigestBeenAdded |= digest.equals(DEFAULT_MGF1_DIGEST);
                         }
+                    } else {
                         /* Because of default MGF1 digest is SHA-1. It has to be added in Key
                          * characteristics. Otherwise, crypto operations will fail with Incompatible
                          * MGF1 digest.
                          */
-                        if (!hasDefaultMgf1DigestBeenAdded) {
-                            importArgs.add(KeyStore2ParameterUtils.makeEnum(
-                                    KeymasterDefs.KM_TAG_RSA_OAEP_MGF_DIGEST,
-                                    KeyProperties.Digest.toKeymaster(DEFAULT_MGF1_DIGEST)
-                            ));
-                        }
+                        importArgs.add(KeyStore2ParameterUtils.makeEnum(
+                                KeymasterDefs.KM_TAG_RSA_OAEP_MGF_DIGEST,
+                                KeyProperties.Digest.toKeymaster(DEFAULT_MGF1_DIGEST)
+                        ));
                     }
                 }
             }
diff --git a/keystore/tests/src/android/security/ParcelableKeyGenParameterSpecTest.java b/keystore/tests/src/android/security/ParcelableKeyGenParameterSpecTest.java
index 2ae61ab..d4e2dbc 100644
--- a/keystore/tests/src/android/security/ParcelableKeyGenParameterSpecTest.java
+++ b/keystore/tests/src/android/security/ParcelableKeyGenParameterSpecTest.java
@@ -17,6 +17,7 @@
 package android.security;
 
 import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThat;
 
@@ -101,6 +102,7 @@
         assertThat(spec.getKeyValidityForOriginationEnd(), is(KEY_VALIDITY_FOR_ORIG_END));
         assertThat(spec.getKeyValidityForConsumptionEnd(), is(KEY_VALIDITY_FOR_CONSUMPTION_END));
         assertThat(spec.getDigests(), is(new String[] {DIGEST}));
+        assertThat(spec.isMgf1DigestsSpecified(), is(false));
         assertThat(spec.getEncryptionPaddings(), is(new String[] {ENCRYPTION_PADDING}));
         assertThat(spec.getSignaturePaddings(), is(new String[] {SIGNATURE_PADDING}));
         assertThat(spec.getBlockModes(), is(new String[] {BLOCK_MODE}));
@@ -189,4 +191,19 @@
         ECGenParameterSpec parcelSpec = (ECGenParameterSpec) fromParcel.getAlgorithmParameterSpec();
         assertEquals(parcelSpec.getName(), ecSpec.getName());
     }
+
+    @Test
+    public void testParcelingMgf1Digests() {
+        String[] mgf1Digests =
+                new String[] {KeyProperties.DIGEST_SHA1, KeyProperties.DIGEST_SHA256};
+
+        ParcelableKeyGenParameterSpec spec = new ParcelableKeyGenParameterSpec(
+                new KeyGenParameterSpec.Builder(ALIAS, KEY_PURPOSES)
+                        .setMgf1Digests(mgf1Digests)
+                        .build());
+        Parcel parcel = parcelForReading(spec);
+        KeyGenParameterSpec fromParcel =
+                ParcelableKeyGenParameterSpec.CREATOR.createFromParcel(parcel).getSpec();
+        assertArrayEquals(fromParcel.getMgf1Digests().toArray(), mgf1Digests);
+    }
 }
diff --git a/keystore/tests/src/android/security/keystore/KeyGenParameterSpecTest.java b/keystore/tests/src/android/security/keystore/KeyGenParameterSpecTest.java
index ddbb1d8..da5e8bf 100644
--- a/keystore/tests/src/android/security/keystore/KeyGenParameterSpecTest.java
+++ b/keystore/tests/src/android/security/keystore/KeyGenParameterSpecTest.java
@@ -16,9 +16,12 @@
 
 package android.security.keystore;
 
+import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertThrows;
 
 import android.security.ParcelableKeyGenParameterSpecTest;
 
@@ -61,4 +64,54 @@
 
         assertEquals(copiedSpec.getAttestationChallenge(), null);
     }
+
+    @Test
+    public void testMgf1DigestsNotSpecifiedByDefault() {
+        KeyGenParameterSpec spec = ParcelableKeyGenParameterSpecTest.configureDefaultSpec();
+        assertThat(spec.isMgf1DigestsSpecified(), is(false));
+        assertThrows(IllegalStateException.class, () -> {
+            spec.getMgf1Digests();
+        });
+    }
+
+    @Test
+    public void testMgf1DigestsCanBeSpecified() {
+        String[] mgf1Digests =
+                new String[] {KeyProperties.DIGEST_SHA1, KeyProperties.DIGEST_SHA256};
+        KeyGenParameterSpec spec =  new KeyGenParameterSpec.Builder(ALIAS, KEY_PURPOSES)
+                .setMgf1Digests(mgf1Digests)
+                .build();
+        assertThat(spec.isMgf1DigestsSpecified(), is(true));
+        assertThat(spec.getMgf1Digests(), containsInAnyOrder(mgf1Digests));
+
+        KeyGenParameterSpec copiedSpec = new KeyGenParameterSpec.Builder(spec).build();
+        assertThat(copiedSpec.isMgf1DigestsSpecified(), is(true));
+        assertThat(copiedSpec.getMgf1Digests(), containsInAnyOrder(mgf1Digests));
+    }
+
+    @Test
+    public void testMgf1DigestsAreNotModified() {
+        String[] mgf1Digests =
+                new String[] {KeyProperties.DIGEST_SHA1, KeyProperties.DIGEST_SHA256};
+        KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(ALIAS, KEY_PURPOSES)
+                .setMgf1Digests(mgf1Digests);
+
+        KeyGenParameterSpec firstSpec =  builder.build();
+        assertArrayEquals(mgf1Digests, firstSpec.getMgf1Digests().toArray());
+
+        String[] otherDigests = new String[] {KeyProperties.DIGEST_SHA224};
+        KeyGenParameterSpec secondSpec =  builder.setMgf1Digests(otherDigests).build();
+        assertThat(secondSpec.getMgf1Digests(), containsInAnyOrder(otherDigests));
+
+        // Now check that the first spec created hasn't changed.
+        assertThat(firstSpec.getMgf1Digests(), containsInAnyOrder(mgf1Digests));
+    }
+
+    @Test
+    public void testEmptyMgf1DigestsCanBeSet() {
+        KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(ALIAS, KEY_PURPOSES)
+                .setMgf1Digests(new String[] {}).build();
+
+        assertThat(spec.isMgf1DigestsSpecified(), is(false));
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java
index 6ea6516..72fc8686 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java
@@ -30,7 +30,7 @@
 import static android.window.StartingWindowInfo.TYPE_PARAMETER_NEW_TASK;
 import static android.window.StartingWindowInfo.TYPE_PARAMETER_PROCESS_RUNNING;
 import static android.window.StartingWindowInfo.TYPE_PARAMETER_TASK_SWITCH;
-import static android.window.StartingWindowInfo.TYPE_PARAMETER_ALLOW_ICON;
+import static android.window.StartingWindowInfo.TYPE_PARAMETER_USE_SOLID_COLOR_SPLASH_SCREEN;
 import static android.window.StartingWindowInfo.TYPE_PARAMETER_WINDOWLESS;
 
 import android.window.StartingWindowInfo;
@@ -52,7 +52,8 @@
         final boolean processRunning = (parameter & TYPE_PARAMETER_PROCESS_RUNNING) != 0;
         final boolean allowTaskSnapshot = (parameter & TYPE_PARAMETER_ALLOW_TASK_SNAPSHOT) != 0;
         final boolean activityCreated = (parameter & TYPE_PARAMETER_ACTIVITY_CREATED) != 0;
-        final boolean allowIcon = (parameter & TYPE_PARAMETER_ALLOW_ICON) != 0;
+        final boolean isSolidColorSplashScreen =
+                (parameter & TYPE_PARAMETER_USE_SOLID_COLOR_SPLASH_SCREEN) != 0;
         final boolean legacySplashScreen =
                 ((parameter & TYPE_PARAMETER_LEGACY_SPLASH_SCREEN) != 0);
         final boolean activityDrawn = (parameter & TYPE_PARAMETER_ACTIVITY_DRAWN) != 0;
@@ -66,13 +67,13 @@
                         + "processRunning=%b, "
                         + "allowTaskSnapshot=%b, "
                         + "activityCreated=%b, "
-                        + "allowIcon=%b, "
+                        + "isSolidColorSplashScreen=%b, "
                         + "legacySplashScreen=%b, "
                         + "activityDrawn=%b, "
                         + "windowless=%b, "
                         + "topIsHome=%b",
                 newTask, taskSwitch, processRunning, allowTaskSnapshot, activityCreated,
-                allowIcon, legacySplashScreen, activityDrawn, windowlessSurface,
+                isSolidColorSplashScreen, legacySplashScreen, activityDrawn, windowlessSurface,
                 topIsHome);
 
         if (windowlessSurface) {
@@ -80,7 +81,7 @@
         }
         if (!topIsHome) {
             if (!processRunning || newTask || (taskSwitch && !activityCreated)) {
-                return getSplashscreenType(allowIcon, legacySplashScreen);
+                return getSplashscreenType(isSolidColorSplashScreen, legacySplashScreen);
             }
         }
 
@@ -94,18 +95,18 @@
                 }
             }
             if (!activityDrawn && !topIsHome) {
-                return getSplashscreenType(allowIcon, legacySplashScreen);
+                return getSplashscreenType(isSolidColorSplashScreen, legacySplashScreen);
             }
         }
         return STARTING_WINDOW_TYPE_NONE;
     }
 
-    private static int getSplashscreenType(boolean allowIcon, boolean legacySplashScreen) {
-        if (allowIcon) {
-            return legacySplashScreen ? STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN
+    private static int getSplashscreenType(boolean solidColorSplashScreen,
+            boolean legacySplashScreen) {
+        return solidColorSplashScreen
+                ? STARTING_WINDOW_TYPE_SOLID_COLOR_SPLASH_SCREEN
+                : legacySplashScreen
+                        ? STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN
                         : STARTING_WINDOW_TYPE_SPLASH_SCREEN;
-        } else {
-            return STARTING_WINDOW_TYPE_SOLID_COLOR_SPLASH_SCREEN;
-        }
     }
 }
diff --git a/libs/hwui/renderthread/HintSessionWrapper.cpp b/libs/hwui/renderthread/HintSessionWrapper.cpp
index d1ebe6d..1c3399a 100644
--- a/libs/hwui/renderthread/HintSessionWrapper.cpp
+++ b/libs/hwui/renderthread/HintSessionWrapper.cpp
@@ -72,6 +72,7 @@
         mSessionValid = true;
         mHintSession = nullptr;
     }
+    mResetsSinceLastReport = 0;
 }
 
 bool HintSessionWrapper::init() {
@@ -109,12 +110,13 @@
     tids.push_back(mUiThreadId);
     tids.push_back(mRenderThreadId);
 
-    // Use a placeholder target value to initialize,
-    // this will always be replaced elsewhere before it gets used
-    int64_t defaultTargetDurationNanos = 16666667;
+    // Use the cached target value if there is one, otherwise use a default. This is to ensure
+    // the cached target and target in PowerHAL are consistent, and that it updates correctly
+    // whenever there is a change.
+    int64_t targetDurationNanos =
+            mLastTargetWorkDuration == 0 ? kDefaultTargetDuration : mLastTargetWorkDuration;
     mHintSessionFuture = CommonPool::async([=, this, tids = std::move(tids)] {
-        return mBinding->createSession(manager, tids.data(), tids.size(),
-                                       defaultTargetDurationNanos);
+        return mBinding->createSession(manager, tids.data(), tids.size(), targetDurationNanos);
     });
     return false;
 }
diff --git a/libs/hwui/renderthread/HintSessionWrapper.h b/libs/hwui/renderthread/HintSessionWrapper.h
index 36e91ea..41891cd 100644
--- a/libs/hwui/renderthread/HintSessionWrapper.h
+++ b/libs/hwui/renderthread/HintSessionWrapper.h
@@ -65,6 +65,7 @@
     static constexpr nsecs_t kResetHintTimeout = 100_ms;
     static constexpr int64_t kSanityCheckLowerBound = 100_us;
     static constexpr int64_t kSanityCheckUpperBound = 10_s;
+    static constexpr int64_t kDefaultTargetDuration = 16666667;
 
     // Allows easier stub when testing
     class HintSessionBinding {
diff --git a/libs/hwui/renderthread/RenderThread.cpp b/libs/hwui/renderthread/RenderThread.cpp
index 94ed06c..f76ea06 100644
--- a/libs/hwui/renderthread/RenderThread.cpp
+++ b/libs/hwui/renderthread/RenderThread.cpp
@@ -17,6 +17,7 @@
 #include "RenderThread.h"
 
 #include <GrContextOptions.h>
+#include <include/gpu/ganesh/gl/GrGLDirectContext.h>
 #include <android-base/properties.h>
 #include <dlfcn.h>
 #include <gl/GrGLInterface.h>
@@ -286,7 +287,7 @@
     auto glesVersion = reinterpret_cast<const char*>(glGetString(GL_VERSION));
     auto size = glesVersion ? strlen(glesVersion) : -1;
     cacheManager().configureContext(&options, glesVersion, size);
-    sk_sp<GrDirectContext> grContext(GrDirectContext::MakeGL(std::move(glInterface), options));
+    sk_sp<GrDirectContext> grContext(GrDirectContexts::MakeGL(std::move(glInterface), options));
     LOG_ALWAYS_FATAL_IF(!grContext.get());
     setGrContext(grContext);
 }
diff --git a/libs/input/tests/Android.bp b/libs/input/tests/Android.bp
index 4eabfb2..8445032 100644
--- a/libs/input/tests/Android.bp
+++ b/libs/input/tests/Android.bp
@@ -26,6 +26,21 @@
     srcs: [
         "PointerController_test.cpp",
     ],
+    sanitize: {
+        hwaddress: true,
+        undefined: true,
+        all_undefined: true,
+        diag: {
+            undefined: true,
+        },
+    },
+    target: {
+        host: {
+            sanitize: {
+                address: true,
+            },
+        },
+    },
     shared_libs: [
         "libandroid_runtime",
         "libinputservice",
diff --git a/libs/input/tests/PointerController_test.cpp b/libs/input/tests/PointerController_test.cpp
index 94faf4a..d9efd3c 100644
--- a/libs/input/tests/PointerController_test.cpp
+++ b/libs/input/tests/PointerController_test.cpp
@@ -148,6 +148,25 @@
     latestPointerDisplayId = displayId;
 }
 
+class TestPointerController : public PointerController {
+public:
+    TestPointerController(sp<android::gui::WindowInfosListener>& registeredListener,
+                          sp<PointerControllerPolicyInterface> policy, const sp<Looper>& looper,
+                          SpriteController& spriteController)
+          : PointerController(
+                    policy, looper, spriteController,
+                    /*enabled=*/true,
+                    [&registeredListener](const sp<android::gui::WindowInfosListener>& listener) {
+                        // Register listener
+                        registeredListener = listener;
+                    },
+                    [&registeredListener](const sp<android::gui::WindowInfosListener>& listener) {
+                        // Unregister listener
+                        if (registeredListener == listener) registeredListener = nullptr;
+                    }) {}
+    ~TestPointerController() override {}
+};
+
 class PointerControllerTest : public Test {
 protected:
     PointerControllerTest();
@@ -159,6 +178,7 @@
     sp<MockPointerControllerPolicyInterface> mPolicy;
     std::unique_ptr<MockSpriteController> mSpriteController;
     std::shared_ptr<PointerController> mPointerController;
+    sp<android::gui::WindowInfosListener> mRegisteredListener;
 
 private:
     void loopThread();
@@ -181,11 +201,12 @@
     EXPECT_CALL(*mSpriteController, createSprite())
             .WillOnce(Return(mPointerSprite));
 
-    mPointerController =
-            PointerController::create(mPolicy, mLooper, *mSpriteController, /*enabled=*/true);
+    mPointerController = std::make_unique<TestPointerController>(mRegisteredListener, mPolicy,
+                                                                 mLooper, *mSpriteController);
 }
 
 PointerControllerTest::~PointerControllerTest() {
+    mPointerController.reset();
     mRunning.store(false, std::memory_order_relaxed);
     mThread.join();
 }
@@ -316,31 +337,16 @@
 
 class PointerControllerWindowInfoListenerTest : public Test {};
 
-class TestPointerController : public PointerController {
-public:
-    TestPointerController(sp<android::gui::WindowInfosListener>& registeredListener,
-                          const sp<Looper>& looper, SpriteController& spriteController)
-          : PointerController(
-                    new MockPointerControllerPolicyInterface(), looper, spriteController,
-                    /*enabled=*/true,
-                    [&registeredListener](const sp<android::gui::WindowInfosListener>& listener) {
-                        // Register listener
-                        registeredListener = listener;
-                    },
-                    [&registeredListener](const sp<android::gui::WindowInfosListener>& listener) {
-                        // Unregister listener
-                        if (registeredListener == listener) registeredListener = nullptr;
-                    }) {}
-};
-
 TEST_F(PointerControllerWindowInfoListenerTest,
        doesNotCrashIfListenerCalledAfterPointerControllerDestroyed) {
     sp<Looper> looper = new Looper(false);
     auto spriteController = NiceMock<MockSpriteController>(looper);
     sp<android::gui::WindowInfosListener> registeredListener;
     sp<android::gui::WindowInfosListener> localListenerCopy;
+    sp<MockPointerControllerPolicyInterface> policy = new MockPointerControllerPolicyInterface();
     {
-        TestPointerController pointerController(registeredListener, looper, spriteController);
+        TestPointerController pointerController(registeredListener, policy, looper,
+                                                spriteController);
         ASSERT_NE(nullptr, registeredListener) << "WindowInfosListener was not registered";
         localListenerCopy = registeredListener;
     }
diff --git a/mime/Android.bp b/mime/Android.bp
index a3ea65c..757862b 100644
--- a/mime/Android.bp
+++ b/mime/Android.bp
@@ -12,7 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-
 package {
     // See: http://go/android-license-faq
     // A large-scale-change added 'default_applicable_licenses' to import
@@ -125,6 +124,6 @@
     srcs: [
         "java-res/vendor.mime.types",
     ],
-    //    strip comments            normalize whitepace       drop empty lines   prepend ? to fields that are missing it
-    cmd: "awk '{gsub(/#.*$$/,\"\"); $$1=$$1; print;}' $(in) | grep ' '         | awk '{for(i=1;i<=NF;i++) { sub(/^\\??/, \"?\", $$i); }; print}' > $(out)",
+    //    strip comments            normalize whitepace       drop empty lines           prepend ? to fields that are missing it
+    cmd: "awk '{gsub(/#.*$$/,\"\"); $$1=$$1; print;}' $(in) | (grep ' ' || echo -n '') | awk '{for(i=1;i<=NF;i++) { sub(/^\\??/, \"?\", $$i); }; print}' > $(out)",
 }
diff --git a/packages/CredentialManager/shared/Android.bp b/packages/CredentialManager/shared/Android.bp
index 38d98a9..0d4af2a 100644
--- a/packages/CredentialManager/shared/Android.bp
+++ b/packages/CredentialManager/shared/Android.bp
@@ -12,6 +12,7 @@
     manifest: "AndroidManifest.xml",
     srcs: ["src/**/*.kt"],
     static_libs: [
+        "androidx.activity_activity-compose",
         "androidx.core_core-ktx",
         "androidx.credentials_credentials",
         "guava",
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/ApiConstants.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/ApiConstants.kt
new file mode 100644
index 0000000..6498ff7
--- /dev/null
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/ApiConstants.kt
@@ -0,0 +1,19 @@
+/*
+ * 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.0N
+ *
+ * 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.credentialmanager
+
+const val IS_AUTO_SELECTED_KEY = "IS_AUTO_SELECTED"
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/IntentParser.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/IntentParser.kt
index defba8d..8986e52 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/IntentParser.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/IntentParser.kt
@@ -17,14 +17,25 @@
 package com.android.credentialmanager
 
 import android.content.Intent
+import android.content.pm.PackageManager
 import android.credentials.ui.RequestInfo
 import com.android.credentialmanager.ktx.requestInfo
 import com.android.credentialmanager.mapper.toGet
 import com.android.credentialmanager.mapper.toRequestCancel
+import com.android.credentialmanager.mapper.toRequestClose
 import com.android.credentialmanager.model.Request
 
-fun Intent.parse(): Request {
-    this.toRequestCancel()?.let { return it }
+fun Intent.parse(
+    packageManager: PackageManager,
+    previousIntent: Intent? = null,
+): Request {
+    this.toRequestClose(packageManager, previousIntent)?.let { closeRequest ->
+        return closeRequest
+    }
+
+    this.toRequestCancel(packageManager)?.let { cancelRequest ->
+        return cancelRequest
+    }
 
     return when (requestInfo?.type) {
         RequestInfo.TYPE_CREATE -> {
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/activity/StartBalIntentSenderForResultContract.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/activity/StartBalIntentSenderForResultContract.kt
new file mode 100644
index 0000000..ef083fd
--- /dev/null
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/activity/StartBalIntentSenderForResultContract.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.credentialmanager.activity
+
+import android.app.ActivityOptions
+import android.content.Context
+import android.content.Intent
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.IntentSenderRequest
+import androidx.activity.result.contract.ActivityResultContract
+import androidx.activity.result.contract.ActivityResultContracts
+
+/**
+ * A custom StartIntentSenderForResult contract implementation that attaches an [ActivityOptions]
+ * that opts in for background activity launch.
+ */
+class StartBalIntentSenderForResultContract :
+    ActivityResultContract<IntentSenderRequest, ActivityResult>() {
+    override fun createIntent(context: Context, input: IntentSenderRequest): Intent {
+        val activityOptionBundle =
+            ActivityOptions.makeBasic().setPendingIntentBackgroundActivityStartMode(
+                ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+            ).toBundle()
+        return Intent(
+            ActivityResultContracts.StartIntentSenderForResult.ACTION_INTENT_SENDER_REQUEST
+        ).putExtra(
+            ActivityResultContracts.StartActivityForResult.EXTRA_ACTIVITY_OPTIONS_BUNDLE,
+            activityOptionBundle
+        ).putExtra(
+            ActivityResultContracts.StartIntentSenderForResult.EXTRA_INTENT_SENDER_REQUEST,
+            input
+        )
+    }
+
+    override fun parseResult(
+        resultCode: Int,
+        intent: Intent?
+    ): ActivityResult = ActivityResult(resultCode, intent)
+}
\ No newline at end of file
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/IntentKtx.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/IntentKtx.kt
index a4c20bf..4533db6 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/IntentKtx.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/IntentKtx.kt
@@ -18,10 +18,12 @@
 
 import android.content.Intent
 import android.credentials.ui.CancelUiRequest
+import android.credentials.ui.Constants
 import android.credentials.ui.CreateCredentialProviderData
 import android.credentials.ui.GetCredentialProviderData
 import android.credentials.ui.ProviderData
 import android.credentials.ui.RequestInfo
+import android.os.ResultReceiver
 
 val Intent.cancelUiRequest: CancelUiRequest?
     get() = this.extras?.getParcelable(
@@ -46,3 +48,9 @@
         ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST,
         CreateCredentialProviderData::class.java
     ) ?: emptyList()
+
+val Intent.resultReceiver: ResultReceiver?
+    get() = this.getParcelableExtra(
+        Constants.EXTRA_RESULT_RECEIVER,
+        ResultReceiver::class.java
+    )
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/mapper/RequestCancelMapper.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/mapper/RequestCancelMapper.kt
index 86a6d23..555a86f 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/mapper/RequestCancelMapper.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/mapper/RequestCancelMapper.kt
@@ -17,13 +17,23 @@
 package com.android.credentialmanager.mapper
 
 import android.content.Intent
+import android.content.pm.PackageManager
+import android.util.Log
+import com.android.credentialmanager.TAG
+import com.android.credentialmanager.ktx.appLabel
 import com.android.credentialmanager.ktx.cancelUiRequest
 import com.android.credentialmanager.model.Request
 
-fun Intent.toRequestCancel(): Request.Cancel? =
+fun Intent.toRequestCancel(packageManager: PackageManager): Request.Cancel? =
     this.cancelUiRequest?.let { cancelUiRequest ->
-        Request.Cancel(
-            showCancellationUi = cancelUiRequest.shouldShowCancellationUi(),
-            appPackageName = cancelUiRequest.appPackageName
-        )
+        val appLabel = packageManager.appLabel(cancelUiRequest.appPackageName)
+        if (appLabel == null) {
+            Log.d(TAG, "Received UI cancel request with an invalid package name.")
+            null
+        } else {
+            Request.Cancel(
+                showCancellationUi = cancelUiRequest.shouldShowCancellationUi(),
+                appName = appLabel
+            )
+        }
     }
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/mapper/RequestCloseMapper.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/mapper/RequestCloseMapper.kt
new file mode 100644
index 0000000..6de3e7d
--- /dev/null
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/mapper/RequestCloseMapper.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.0N
+ *
+ * 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.credentialmanager.mapper
+
+import android.content.Intent
+import android.content.pm.PackageManager
+import com.android.credentialmanager.ktx.requestInfo
+import com.android.credentialmanager.model.Request
+
+fun Intent.toRequestClose(
+    packageManager: PackageManager,
+    previousIntent: Intent? = null,
+): Request.Close? {
+    // Close request comes as "Cancel" request from Credential Manager API
+    val currentRequest = toRequestCancel(packageManager = packageManager) ?: return null
+
+    if (currentRequest.showCancellationUi) {
+        // Current request is to Cancel and not to Close
+        return null
+    }
+
+    previousIntent?.let {
+        val previousToken = previousIntent.requestInfo?.token
+        val currentToken = this.requestInfo?.token
+
+        if (previousToken != currentToken) {
+            // Current cancellation is for a different request, don't close the current flow.
+            return null
+        }
+    }
+
+    return Request.Close
+}
\ No newline at end of file
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/mapper/RequestGetMapper.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/mapper/RequestGetMapper.kt
index ed9d563..ee45fbb 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/mapper/RequestGetMapper.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/mapper/RequestGetMapper.kt
@@ -1,22 +1,66 @@
+/*
+ * 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.0N
+ *
+ * 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.credentialmanager.mapper
 
 import android.content.Intent
+import android.credentials.ui.Entry
 import android.credentials.ui.GetCredentialProviderData
+import androidx.credentials.provider.PasswordCredentialEntry
+import com.android.credentialmanager.factory.fromSlice
 import com.android.credentialmanager.ktx.getCredentialProviderDataList
+import com.android.credentialmanager.ktx.requestInfo
+import com.android.credentialmanager.ktx.resultReceiver
+import com.android.credentialmanager.model.Password
 import com.android.credentialmanager.model.Request
 import com.google.common.collect.ImmutableList
 import com.google.common.collect.ImmutableMap
 
-fun Intent.toGet() = Request.Get(
-    providers = ImmutableMap.copyOf(
-        getCredentialProviderDataList.associateBy { it.providerFlattenedComponentName }
-    ),
-    entries = ImmutableList.copyOf(
-        getCredentialProviderDataList.map { providerData ->
-            check(providerData is GetCredentialProviderData) {
-                "Invalid provider data type for GetCredentialRequest"
+fun Intent.toGet(): Request.Get {
+    val credentialEntries = mutableListOf<Pair<String, Entry>>()
+    for (providerData in getCredentialProviderDataList) {
+        if (providerData is GetCredentialProviderData) {
+            for (credentialEntry in providerData.credentialEntries) {
+                credentialEntries.add(
+                    Pair(providerData.providerFlattenedComponentName, credentialEntry)
+                )
             }
-            providerData
-        }.flatMap { it.credentialEntries }
+        }
+    }
+
+    val passwordEntries = mutableListOf<Password>()
+    for ((providerId, entry) in credentialEntries) {
+        val slice = fromSlice(entry.slice)
+        if (slice is PasswordCredentialEntry) {
+            passwordEntries.add(
+                Password(
+                    providerId = providerId,
+                    entry = entry,
+                    passwordCredentialEntry = slice
+                )
+            )
+        }
+    }
+
+    return Request.Get(
+        token = requestInfo?.token,
+        resultReceiver = this.resultReceiver,
+        providers = ImmutableMap.copyOf(
+            getCredentialProviderDataList.associateBy { it.providerFlattenedComponentName }
+        ),
+        passwordEntries = ImmutableList.copyOf(passwordEntries)
     )
-)
\ No newline at end of file
+}
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/model/Password.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/Password.kt
new file mode 100644
index 0000000..2fe4fd5
--- /dev/null
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/Password.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.0N
+ *
+ * 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.credentialmanager.model
+
+import android.credentials.ui.Entry
+import androidx.credentials.provider.PasswordCredentialEntry
+
+data class Password(
+    val providerId: String,
+    val entry: Entry,
+    val passwordCredentialEntry: PasswordCredentialEntry,
+)
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/model/Request.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/Request.kt
index bc07310..6011a1c 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/model/Request.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/Request.kt
@@ -16,8 +16,9 @@
 
 package com.android.credentialmanager.model
 
-import android.credentials.ui.Entry
 import android.credentials.ui.ProviderData
+import android.os.IBinder
+import android.os.ResultReceiver
 import com.google.common.collect.ImmutableList
 import com.google.common.collect.ImmutableMap
 
@@ -25,15 +26,33 @@
  * Represents the request made by the CredentialManager API.
  */
 sealed class Request {
+
+    /**
+     * Request to close the app without displaying a message to the user and without reporting
+     * anything back to the Credential Manager service.
+     */
+    data object Close : Request()
+
+    /**
+     * Request to close the app, displaying a message to the user.
+     */
     data class Cancel(
         val showCancellationUi: Boolean,
-        val appPackageName: String?
+        val appName: String
     ) : Request()
 
+    /**
+     * Request to start the get credentials flow.
+     */
     data class Get(
+        val token: IBinder?,
+        val resultReceiver: ResultReceiver?,
         val providers: ImmutableMap<String, ProviderData>,
-        val entries: ImmutableList<Entry>,
+        val passwordEntries: ImmutableList<Password>,
     ) : Request()
 
+    /**
+     * Request to start the create credentials flow.
+     */
     data object Create : Request()
 }
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/repository/RequestRepository.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/repository/RequestRepository.kt
new file mode 100644
index 0000000..5ab5ab9
--- /dev/null
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/repository/RequestRepository.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.0N
+ *
+ * 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.credentialmanager.repository
+
+import android.app.Application
+import android.content.Intent
+import android.util.Log
+import com.android.credentialmanager.TAG
+import com.android.credentialmanager.model.Request
+import com.android.credentialmanager.parse
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+class RequestRepository(
+    private val application: Application,
+) {
+
+    private val _requests = MutableStateFlow<Request?>(null)
+    val requests: StateFlow<Request?> = _requests
+
+    suspend fun processRequest(intent: Intent, previousIntent: Intent? = null) {
+        val request = intent.parse(
+            packageManager = application.packageManager,
+            previousIntent = previousIntent
+        )
+
+        Log.d(TAG, "Request parsed: $request")
+
+        _requests.value = request
+    }
+}
diff --git a/packages/CredentialManager/wear/Android.bp b/packages/CredentialManager/wear/Android.bp
index c0dff16..e5f5cc2 100644
--- a/packages/CredentialManager/wear/Android.bp
+++ b/packages/CredentialManager/wear/Android.bp
@@ -37,6 +37,7 @@
         "androidx.lifecycle_lifecycle-extensions",
         "androidx.lifecycle_lifecycle-livedata",
         "androidx.lifecycle_lifecycle-runtime-ktx",
+        "androidx.lifecycle_lifecycle-runtime-compose",
         "androidx.lifecycle_lifecycle-viewmodel-compose",
         "androidx.wear.compose_compose-foundation",
         "androidx.wear.compose_compose-material",
diff --git a/packages/CredentialManager/wear/AndroidManifest.xml b/packages/CredentialManager/wear/AndroidManifest.xml
index 90248734..b480ac3 100644
--- a/packages/CredentialManager/wear/AndroidManifest.xml
+++ b/packages/CredentialManager/wear/AndroidManifest.xml
@@ -1,4 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
+
 <!--
 /*
  * Copyright (c) 2023 Google Inc.
@@ -21,25 +22,27 @@
 
     <uses-feature android:name="android.hardware.type.watch" />
 
-    <uses-permission android:name="android.permission.LAUNCH_CREDENTIAL_SELECTOR"/>
-    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
-    <uses-permission android:name="android.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS"/>
+    <uses-permission android:name="android.permission.LAUNCH_CREDENTIAL_SELECTOR" />
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+    <uses-permission android:name="android.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS" />
 
     <application
-      android:allowBackup="true"
-      android:dataExtractionRules="@xml/data_extraction_rules"
-      android:fullBackupContent="@xml/backup_rules"
-      android:label="@string/app_name"
-      android:supportsRtl="true">
+        android:name=".CredentialSelectorApp"
+        android:allowBackup="true"
+        android:dataExtractionRules="@xml/data_extraction_rules"
+        android:fullBackupContent="@xml/backup_rules"
+        android:label="@string/app_name"
+        android:supportsRtl="true">
 
+        <!-- Activity called by GMS has to be exactly:
+        com.android.credentialmanager.CredentialSelectorActivity -->
         <activity
-            android:name=".ui.CredentialSelectorActivity"
+            android:name=".CredentialSelectorActivity"
+            android:excludeFromRecents="true"
             android:exported="true"
-            android:permission="android.permission.LAUNCH_CREDENTIAL_SELECTOR"
-            android:launchMode="singleTop"
             android:label="@string/app_name"
-            android:excludeFromRecents="true">
-        </activity>
-  </application>
+            android:launchMode="singleTop"
+            android:permission="android.permission.LAUNCH_CREDENTIAL_SELECTOR" />
+    </application>
 
 </manifest>
diff --git a/packages/CredentialManager/wear/res/values/themes.xml b/packages/CredentialManager/wear/res/values/themes.xml
deleted file mode 100644
index 22329e9f..0000000
--- a/packages/CredentialManager/wear/res/values/themes.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2023 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<resources>
-  <style name="Theme.CredentialSelector" parent="@*android:style/ThemeOverlay.DeviceDefault.Accent.DayNight">
-    <item name="android:windowContentOverlay">@null</item>
-    <item name="android:windowNoTitle">true</item>
-    <item name="android:windowBackground">@android:color/transparent</item>
-    <item name="android:windowIsTranslucent">true</item>
-  </style>
-</resources>
\ No newline at end of file
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt
new file mode 100644
index 0000000..273d0b1
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt
@@ -0,0 +1,100 @@
+/*
+ * 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.0N
+ *
+ * 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.credentialmanager
+
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.viewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.wear.compose.material.MaterialTheme
+import com.android.credentialmanager.ui.WearApp
+import com.android.credentialmanager.ui.screens.single.password.SinglePasswordScreen
+import com.google.android.horologist.annotations.ExperimentalHorologistApi
+import com.google.android.horologist.compose.layout.belowTimeTextPreview
+import kotlinx.coroutines.launch
+
+class CredentialSelectorActivity : ComponentActivity() {
+
+    private val viewModel: CredentialSelectorViewModel by viewModels {
+        CredentialSelectorViewModel.Factory
+    }
+
+    @OptIn(ExperimentalHorologistApi::class)
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        setTheme(android.R.style.Theme_DeviceDefault)
+
+        // TODO: b/301027810 due to this issue with compose in Main platform, we are implementing a
+        // workaround. Once the issue is fixed, remove the "else" bracket and leave only the
+        // contents of the "if" bracket.
+        if (false) {
+            setContent {
+                MaterialTheme {
+                    WearApp(
+                        viewModel = viewModel,
+                        onCloseApp = ::finish,
+                    )
+                }
+            }
+        } else {
+            // TODO: b/301027810 Remove the content of this "else" bracket fully once issue is fixed
+            lifecycleScope.launch {
+                repeatOnLifecycle(Lifecycle.State.STARTED) {
+                    viewModel.uiState.collect { uiState ->
+                        when (uiState) {
+                            CredentialSelectorUiState.Idle -> {
+                                // Don't display anything, assuming that there should be minimal latency
+                                // to parse the Credential Manager intent and define the state of the
+                                // app. If latency is big, then a "loading" screen should be displayed
+                                // to the user.
+                            }
+
+                            is CredentialSelectorUiState.Get -> {
+                                setContent {
+                                    MaterialTheme {
+                                        SinglePasswordScreen(
+                                            columnState = belowTimeTextPreview(),
+                                            onCloseApp = ::finish,
+                                        )
+                                    }
+                                }
+                            }
+
+                            else -> finish()
+                        }
+                    }
+                }
+            }
+        }
+
+        viewModel.onNewIntent(intent)
+    }
+
+    override fun onNewIntent(intent: Intent) {
+        super.onNewIntent(intent)
+
+        val previousIntent = getIntent()
+        setIntent(intent)
+
+        viewModel.onNewIntent(intent, previousIntent)
+    }
+}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorApp.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorApp.kt
new file mode 100644
index 0000000..7c81fd0
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorApp.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.0N
+ *
+ * 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.credentialmanager
+
+import android.app.Application
+import com.android.credentialmanager.di.inject
+import com.android.credentialmanager.repository.RequestRepository
+
+class CredentialSelectorApp : Application() {
+
+    lateinit var requestRepository: RequestRepository
+
+    override fun onCreate() {
+        super.onCreate()
+
+        inject()
+    }
+}
\ No newline at end of file
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
new file mode 100644
index 0000000..d557dc0
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.0N
+ *
+ * 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.credentialmanager
+
+import android.content.Intent
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
+import androidx.lifecycle.viewModelScope
+import androidx.lifecycle.viewmodel.CreationExtras
+import com.android.credentialmanager.model.Request
+import com.android.credentialmanager.repository.RequestRepository
+import com.android.credentialmanager.ui.mappers.toGet
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+class CredentialSelectorViewModel(
+    private val requestRepository: RequestRepository,
+) : ViewModel() {
+
+    val uiState: StateFlow<CredentialSelectorUiState> = requestRepository.requests
+        .map { request ->
+            when (request) {
+                null -> CredentialSelectorUiState.Idle
+                is Request.Cancel -> CredentialSelectorUiState.Cancel(request.appName)
+                Request.Close -> CredentialSelectorUiState.Close
+                Request.Create -> CredentialSelectorUiState.Create
+                is Request.Get -> request.toGet()
+            }
+        }
+        .stateIn(
+            viewModelScope,
+            started = SharingStarted.WhileSubscribed(5000),
+            initialValue = CredentialSelectorUiState.Idle,
+        )
+
+    fun onNewIntent(intent: Intent, previousIntent: Intent? = null) {
+        viewModelScope.launch {
+            requestRepository.processRequest(intent = intent, previousIntent = previousIntent)
+        }
+    }
+
+    companion object {
+        val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
+            @Suppress("UNCHECKED_CAST")
+            override fun <T : ViewModel> create(
+                modelClass: Class<T>,
+                extras: CreationExtras
+            ): T {
+                val application = checkNotNull(extras[APPLICATION_KEY])
+
+                return CredentialSelectorViewModel(
+                    requestRepository = (application as CredentialSelectorApp).requestRepository,
+                ) as T
+            }
+        }
+    }
+}
+
+sealed class CredentialSelectorUiState {
+    data object Idle : CredentialSelectorUiState()
+    sealed class Get : CredentialSelectorUiState() {
+        data object SingleProviderSinglePasskey : Get()
+        data object SingleProviderSinglePassword : Get()
+
+        // TODO: b/301206470 add the remaining states
+    }
+
+    data object Create : CredentialSelectorUiState()
+    data class Cancel(val appName: String) : CredentialSelectorUiState()
+    data object Close : CredentialSelectorUiState()
+}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/di/DI.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/di/DI.kt
new file mode 100644
index 0000000..a11017b
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/di/DI.kt
@@ -0,0 +1,17 @@
+package com.android.credentialmanager.di
+
+import android.app.Application
+import com.android.credentialmanager.CredentialSelectorApp
+import com.android.credentialmanager.repository.RequestRepository
+
+// TODO b/301601582 add Hilt for dependency injection
+
+fun CredentialSelectorApp.inject() {
+    requestRepository = requestRepository(application = this)
+}
+
+private fun requestRepository(
+    application: Application,
+): RequestRepository = RequestRepository(
+    application = application,
+)
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/CredentialSelectorActivity.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/CredentialSelectorActivity.kt
deleted file mode 100644
index 53122ba..0000000
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/CredentialSelectorActivity.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0N
- *
- * 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.credentialmanager.ui
-
-import android.content.Intent
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.activity.viewModels
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import androidx.wear.compose.material.MaterialTheme
-import kotlinx.coroutines.launch
-
-class CredentialSelectorActivity : ComponentActivity() {
-
-    private val viewModel: CredentialSelectorViewModel by viewModels()
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-
-        setTheme(android.R.style.Theme_DeviceDefault)
-
-        lifecycleScope.launch {
-            repeatOnLifecycle(Lifecycle.State.STARTED) {
-                viewModel.uiState.collect { uiState ->
-                    when (uiState) {
-                        CredentialSelectorUiState.Idle -> {
-                            // Don't display anything, assuming that there should be minimal latency
-                            // to parse the Credential Manager intent and define the state of the
-                            // app. If latency is big, then a "loading" screen should be displayed
-                            // to the user.
-                        }
-
-                        is CredentialSelectorUiState.Get -> {
-                            setContent {
-                                MaterialTheme {
-                                    WearApp()
-                                }
-                            }
-                        }
-
-                        CredentialSelectorUiState.Create -> {
-                            // TODO: b/301206624 - Implement create flow
-                            finish()
-                        }
-
-                        is CredentialSelectorUiState.Cancel -> {
-                            // TODO: b/300422310 - Implement cancel with message flow
-                            finish()
-                        }
-
-                        CredentialSelectorUiState.Finish -> {
-                            finish()
-                        }
-                    }
-                }
-            }
-        }
-
-        viewModel.onNewIntent(intent)
-    }
-
-    override fun onNewIntent(intent: Intent) {
-        super.onNewIntent(intent)
-
-        val previousIntent = getIntent()
-        setIntent(intent)
-
-        viewModel.onNewIntent(intent, previousIntent)
-    }
-}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/CredentialSelectorViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/CredentialSelectorViewModel.kt
deleted file mode 100644
index d22d5d1..0000000
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/CredentialSelectorViewModel.kt
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0N
- *
- * 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.credentialmanager.ui
-
-import android.app.Application
-import android.content.Intent
-import android.util.Log
-import androidx.lifecycle.AndroidViewModel
-import androidx.lifecycle.viewModelScope
-import com.android.credentialmanager.TAG
-import com.android.credentialmanager.parse
-import com.android.credentialmanager.ktx.appLabel
-import com.android.credentialmanager.ktx.requestInfo
-import com.android.credentialmanager.mapper.toGet
-import com.android.credentialmanager.ui.model.PasskeyUiModel
-import com.android.credentialmanager.ui.model.PasswordUiModel
-import com.android.credentialmanager.model.Request
-import com.android.credentialmanager.ui.mapper.toGet
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.launch
-
-class CredentialSelectorViewModel(
-    private val application: Application
-) : AndroidViewModel(application = application) {
-
-    private val _uiState =
-        MutableStateFlow<CredentialSelectorUiState>(CredentialSelectorUiState.Idle)
-    val uiState: StateFlow<CredentialSelectorUiState> = _uiState
-
-    fun onNewIntent(intent: Intent, previousIntent: Intent? = null) {
-        viewModelScope.launch {
-            val request = intent.parse()
-            if (shouldFinishActivity(request = request, previousIntent = previousIntent)) {
-                _uiState.value = CredentialSelectorUiState.Finish
-            } else {
-                when (request) {
-                    is Request.Cancel -> {
-                        request.appPackageName?.let { appPackageName ->
-                            application.packageManager.appLabel(appPackageName)?.let { appLabel ->
-                                _uiState.value = CredentialSelectorUiState.Cancel(appLabel)
-                            } ?: run {
-                                Log.d(TAG,
-                                    "Received UI cancel request with an invalid package name.")
-                                _uiState.value = CredentialSelectorUiState.Finish
-                            }
-                        } ?: run {
-                            Log.d(TAG, "Received UI cancel request with an invalid package name.")
-                            _uiState.value = CredentialSelectorUiState.Finish
-                        }
-                    }
-
-                    Request.Create -> {
-                        _uiState.value = CredentialSelectorUiState.Create
-                    }
-
-                    is Request.Get -> {
-                        _uiState.value = request.toGet()
-                    }
-                }
-            }
-        }
-    }
-
-    /**
-     * Check if backend requested the UI activity to be cancelled. Different from the other
-     * finishing flows, this one does not report anything back to the Credential Manager service
-     * backend.
-     */
-    private fun shouldFinishActivity(request: Request, previousIntent: Intent? = null): Boolean {
-        if (request !is Request.Cancel) {
-            return false
-        } else {
-            Log.d(
-                TAG, "Received UI cancellation intent. Should show cancellation" +
-                " ui = ${request.showCancellationUi}")
-
-            previousIntent?.let {
-                val previousUiRequest = previousIntent.parse()
-
-                if (previousUiRequest is Request.Cancel) {
-                    val previousToken = previousIntent.requestInfo?.token
-                    val currentToken = previousIntent.requestInfo?.token
-
-                    if (previousToken != currentToken) {
-                        // Cancellation was for a different request, don't cancel the current UI.
-                        return false
-                    }
-                }
-            }
-
-            return !request.showCancellationUi
-        }
-    }
-}
-
-sealed class CredentialSelectorUiState {
-    data object Idle : CredentialSelectorUiState()
-    sealed class Get : CredentialSelectorUiState() {
-        data class SingleProviderSinglePasskey(val passkeyUiModel: PasskeyUiModel) : Get()
-        data class SingleProviderSinglePassword(val passwordUiModel: PasswordUiModel) : Get()
-
-        // TODO: b/301206470 add the remaining states
-    }
-
-    data object Create : CredentialSelectorUiState()
-    data class Cancel(val appName: String) : CredentialSelectorUiState()
-    data object Finish : CredentialSelectorUiState()
-}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/Navigation.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/Navigation.kt
new file mode 100644
index 0000000..da5697d
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/Navigation.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.0N
+ *
+ * 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.credentialmanager.ui
+
+import androidx.navigation.NavController
+
+fun NavController.navigateToLoading() {
+    navigate(Screen.Loading.route)
+}
+
+fun NavController.navigateToSinglePasswordScreen() {
+    navigate(Screen.SinglePasswordScreen.route)
+}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/Screen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/Screen.kt
index 7d1a49b..c3919a0 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/Screen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/Screen.kt
@@ -19,5 +19,7 @@
 sealed class Screen(
     val route: String,
 ) {
-    data object Main : Screen("main")
+    data object Loading : Screen("loading")
+
+    data object SinglePasswordScreen : Screen("singlePasswordScreen")
 }
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt
index 19ea9ed..7e0ea30 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt
@@ -19,28 +19,94 @@
 package com.android.credentialmanager.ui
 
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.navigation.NavController
 import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
 import androidx.wear.compose.navigation.rememberSwipeDismissableNavController
 import androidx.wear.compose.navigation.rememberSwipeDismissableNavHostState
-import com.android.credentialmanager.ui.screens.MainScreen
+import com.android.credentialmanager.CredentialSelectorUiState
+import com.android.credentialmanager.CredentialSelectorViewModel
+import com.android.credentialmanager.ui.screens.LoadingScreen
+import com.android.credentialmanager.ui.screens.single.password.SinglePasswordScreen
 import com.google.android.horologist.annotations.ExperimentalHorologistApi
 import com.google.android.horologist.compose.navscaffold.WearNavScaffold
 import com.google.android.horologist.compose.navscaffold.composable
+import com.google.android.horologist.compose.navscaffold.scrollable
 
 @Composable
-fun WearApp() {
+fun WearApp(
+    viewModel: CredentialSelectorViewModel,
+    onCloseApp: () -> Unit,
+) {
     val navController = rememberSwipeDismissableNavController()
     val swipeToDismissBoxState = rememberSwipeToDismissBoxState()
     val navHostState =
         rememberSwipeDismissableNavHostState(swipeToDismissBoxState = swipeToDismissBoxState)
 
+    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
     WearNavScaffold(
-        startDestination = Screen.Main.route,
+        startDestination = Screen.Loading.route,
         navController = navController,
         state = navHostState,
     ) {
-        composable(Screen.Main.route) {
-            MainScreen()
+        composable(Screen.Loading.route) {
+            LoadingScreen()
+        }
+
+        scrollable(Screen.SinglePasswordScreen.route) {
+            SinglePasswordScreen(
+                columnState = it.columnState,
+                onCloseApp = onCloseApp,
+            )
+        }
+    }
+
+    when (val state = uiState) {
+        CredentialSelectorUiState.Idle -> {
+            if (navController.currentDestination?.route != Screen.Loading.route) {
+                navController.navigateToLoading()
+            }
+        }
+
+        is CredentialSelectorUiState.Get -> {
+            handleGetNavigation(
+                navController = navController,
+                state = state,
+                onCloseApp = onCloseApp,
+            )
+        }
+
+        CredentialSelectorUiState.Create -> {
+            // TODO: b/301206624 - Implement create flow
+            onCloseApp()
+        }
+
+        is CredentialSelectorUiState.Cancel -> {
+            // TODO: b/300422310 - Implement cancel with message flow
+            onCloseApp()
+        }
+
+        CredentialSelectorUiState.Close -> {
+            onCloseApp()
+        }
+    }
+}
+
+private fun handleGetNavigation(
+    navController: NavController,
+    state: CredentialSelectorUiState.Get,
+    onCloseApp: () -> Unit,
+) {
+    when (state) {
+        is CredentialSelectorUiState.Get.SingleProviderSinglePassword -> {
+            navController.navigateToSinglePasswordScreen()
+        }
+
+        else -> {
+            // TODO: b/301206470 - Implement other get flows
+            onCloseApp()
         }
     }
 }
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mapper/CredentialSelectorUiStateGetMapper.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mapper/CredentialSelectorUiStateGetMapper.kt
deleted file mode 100644
index 5ceec178..0000000
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mapper/CredentialSelectorUiStateGetMapper.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-package com.android.credentialmanager.ui.mapper
-
-import androidx.credentials.provider.CustomCredentialEntry
-import androidx.credentials.provider.PasswordCredentialEntry
-import androidx.credentials.provider.PublicKeyCredentialEntry
-import com.android.credentialmanager.ui.CredentialSelectorUiState
-import com.android.credentialmanager.factory.fromSlice
-import com.android.credentialmanager.ui.model.PasswordUiModel
-import com.android.credentialmanager.model.Request
-
-fun Request.Get.toGet(): CredentialSelectorUiState.Get {
-    if (this.providers.isEmpty()) {
-        throw IllegalStateException("Invalid GetCredential request with empty list of providers.")
-    }
-
-    if (this.entries.isEmpty()) {
-        throw IllegalStateException("Invalid GetCredential request with empty list of entries.")
-    }
-
-    if (this.providers.size == 1) {
-        if (this.entries.size == 1) {
-            val slice = this.entries.first().slice
-            when (val credentialEntry = fromSlice(slice)) {
-                is PasswordCredentialEntry -> {
-                    return CredentialSelectorUiState.Get.SingleProviderSinglePassword(
-                        PasswordUiModel(credentialEntry.displayName.toString())
-                    )
-                }
-
-                is PublicKeyCredentialEntry -> {
-                    TODO("b/301206470 - to be implemented")
-                }
-
-                is CustomCredentialEntry -> {
-                    TODO("b/301206470 - to be implemented")
-                }
-
-                else -> {
-                    throw IllegalStateException(
-                        "Encountered unrecognized credential entry (${slice.spec?.type}) for " +
-                            "GetCredential request with single account"
-                    )
-                }
-            }
-        } else {
-            TODO("b/301206470 - to be implemented")
-        }
-    } else {
-        TODO("b/301206470 - to be implemented")
-    }
-}
\ No newline at end of file
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mappers/CredentialSelectorUiStateGetMapper.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mappers/CredentialSelectorUiStateGetMapper.kt
new file mode 100644
index 0000000..f2f878e
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mappers/CredentialSelectorUiStateGetMapper.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.0N
+ *
+ * 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.credentialmanager.ui.mappers
+
+import com.android.credentialmanager.model.Request
+import com.android.credentialmanager.CredentialSelectorUiState
+
+fun Request.Get.toGet(): CredentialSelectorUiState.Get {
+    // TODO: b/301206470 returning a hard coded state for MVP
+    if (true) return CredentialSelectorUiState.Get.SingleProviderSinglePassword
+
+    return if (providers.size == 1) {
+        if (passwordEntries.size == 1) {
+            CredentialSelectorUiState.Get.SingleProviderSinglePassword
+        } else {
+            TODO() // b/301206470 - Implement other get flows
+        }
+    } else {
+        TODO() // b/301206470 - Implement other get flows
+    }
+}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/MainScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/LoadingScreen.kt
similarity index 74%
rename from packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/MainScreen.kt
rename to packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/LoadingScreen.kt
index 94a671e..b3ab0c4 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/MainScreen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/LoadingScreen.kt
@@ -16,17 +16,15 @@
 
 package com.android.credentialmanager.ui.screens
 
-import androidx.compose.foundation.layout.Box
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.wear.compose.material.Text
 
 @Composable
-fun MainScreen(
+fun LoadingScreen(
     modifier: Modifier = Modifier
 ) {
-    Box(modifier = modifier, contentAlignment = Alignment.Center) {
-        Text("This is a placeholder for the main screen.")
-    }
+    // Don't display anything, assuming that there should be minimal latency
+    // to parse the Credential Manager intent and define the state of the
+    // app. If latency is big, then a "loading" screen should be displayed
+    // to the user.
 }
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/SinglePasswordScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/SinglePasswordScreen.kt
deleted file mode 100644
index d863d3c..0000000
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/SinglePasswordScreen.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright 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
- *
- *      https://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.
- */
-
-@file:OptIn(ExperimentalHorologistApi::class)
-
-package com.android.credentialmanager.ui.screens
-
-import androidx.compose.foundation.layout.padding
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import com.android.credentialmanager.R
-import com.android.credentialmanager.ui.components.DialogButtonsRow
-import com.android.credentialmanager.ui.components.PasswordRow
-import com.android.credentialmanager.ui.components.SignInHeader
-import com.google.android.horologist.annotations.ExperimentalHorologistApi
-import com.google.android.horologist.compose.layout.ScalingLazyColumnState
-import com.google.android.horologist.compose.layout.belowTimeTextPreview
-import com.google.android.horologist.compose.tools.WearPreview
-
-@Composable
-fun SinglePasswordScreen(
-    email: String,
-    onCancelClick: () -> Unit,
-    onOKClick: () -> Unit,
-    columnState: ScalingLazyColumnState,
-    modifier: Modifier = Modifier,
-) {
-    SingleAccountScreen(
-        headerContent = {
-            SignInHeader(
-                icon = R.drawable.passkey_icon,
-                title = stringResource(R.string.use_password_title),
-            )
-        },
-        accountContent = {
-            PasswordRow(
-                email = email,
-                modifier = Modifier.padding(top = 10.dp),
-            )
-        },
-        columnState = columnState,
-        modifier = modifier.padding(horizontal = 10.dp)
-    ) {
-        item {
-            DialogButtonsRow(
-                onCancelClick = onCancelClick,
-                onOKClick = onOKClick,
-                modifier = Modifier.padding(top = 10.dp)
-            )
-        }
-    }
-}
-
-@WearPreview
-@Composable
-fun SinglePasswordScreenPreview() {
-    SinglePasswordScreen(
-        email = "beckett_bakery@gmail.com",
-        onCancelClick = {},
-        onOKClick = {},
-        columnState = belowTimeTextPreview(),
-    )
-}
-
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/SingleAccountScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/SingleAccountScreen.kt
similarity index 97%
rename from packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/SingleAccountScreen.kt
rename to packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/SingleAccountScreen.kt
index f344ad0..8532783 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/SingleAccountScreen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/SingleAccountScreen.kt
@@ -16,7 +16,7 @@
 
 @file:OptIn(ExperimentalHorologistApi::class)
 
-package com.android.credentialmanager.ui.screens
+package com.android.credentialmanager.ui.screens.single
 
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/SinglePasskeyScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt
similarity index 94%
rename from packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/SinglePasskeyScreen.kt
rename to packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt
index c8f871e..c9b0230 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/SinglePasskeyScreen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt
@@ -16,7 +16,7 @@
 
 @file:OptIn(ExperimentalHorologistApi::class)
 
-package com.android.credentialmanager.ui.screens
+package com.android.credentialmanager.ui.screens.single.passkey
 
 import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
@@ -27,6 +27,7 @@
 import com.android.credentialmanager.ui.components.AccountRow
 import com.android.credentialmanager.ui.components.DialogButtonsRow
 import com.android.credentialmanager.ui.components.SignInHeader
+import com.android.credentialmanager.ui.screens.single.SingleAccountScreen
 import com.google.android.horologist.annotations.ExperimentalHorologistApi
 import com.google.android.horologist.compose.layout.ScalingLazyColumnState
 import com.google.android.horologist.compose.layout.belowTimeTextPreview
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt
new file mode 100644
index 0000000..c885ec4
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright 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
+ *
+ *      https://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.
+ */
+
+@file:OptIn(ExperimentalHorologistApi::class)
+
+package com.android.credentialmanager.ui.screens.single.password
+
+import android.util.Log
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.android.credentialmanager.R
+import com.android.credentialmanager.TAG
+import com.android.credentialmanager.activity.StartBalIntentSenderForResultContract
+import com.android.credentialmanager.ui.components.DialogButtonsRow
+import com.android.credentialmanager.ui.components.PasswordRow
+import com.android.credentialmanager.ui.components.SignInHeader
+import com.android.credentialmanager.ui.screens.single.SingleAccountScreen
+import com.google.android.horologist.annotations.ExperimentalHorologistApi
+import com.google.android.horologist.compose.layout.ScalingLazyColumnState
+import com.google.android.horologist.compose.layout.belowTimeTextPreview
+import com.google.android.horologist.compose.tools.WearPreview
+
+@Composable
+fun SinglePasswordScreen(
+    columnState: ScalingLazyColumnState,
+    onCloseApp: () -> Unit,
+    modifier: Modifier = Modifier,
+    viewModel: SinglePasswordScreenViewModel =
+        viewModel(factory = SinglePasswordScreenViewModel.Factory),
+) {
+    viewModel.initialize()
+
+    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+    when (val state = uiState) {
+        SinglePasswordScreenUiState.Idle -> {
+            // TODO: b/301206470 implement latency version of the screen
+        }
+
+        is SinglePasswordScreenUiState.Loaded -> {
+            val model = state.passwordUiModel
+            SinglePasswordScreen(
+                email = model.email,
+                onCancelClick = viewModel::onCancelClick,
+                onOKClick = viewModel::onOKClick,
+                columnState = columnState,
+                modifier = modifier
+            )
+        }
+
+        is SinglePasswordScreenUiState.PasswordSelected -> {
+            val launcher = rememberLauncherForActivityResult(
+                StartBalIntentSenderForResultContract()
+            ) {
+                viewModel.onPasswordInfoRetrieved(it.resultCode, it.data)
+            }
+
+            SideEffect {
+                launcher.launch(state.intentSenderRequest)
+            }
+        }
+
+        SinglePasswordScreenUiState.Cancel -> {
+            // TODO: b/301206470 implement navigation for when user taps cancel
+        }
+
+        SinglePasswordScreenUiState.Error -> {
+            // TODO: b/301206470 implement navigation for when there is an error to load screen
+        }
+
+        SinglePasswordScreenUiState.Completed -> {
+            Log.d(TAG, "Received signal to finish the activity.")
+            onCloseApp()
+        }
+    }
+}
+
+@Composable
+fun SinglePasswordScreen(
+    email: String,
+    onCancelClick: () -> Unit,
+    onOKClick: () -> Unit,
+    columnState: ScalingLazyColumnState,
+    modifier: Modifier = Modifier,
+) {
+    SingleAccountScreen(
+        headerContent = {
+            SignInHeader(
+                icon = R.drawable.passkey_icon,
+                title = stringResource(R.string.use_password_title),
+            )
+        },
+        accountContent = {
+            PasswordRow(
+                email = email,
+                modifier = Modifier.padding(top = 10.dp),
+            )
+        },
+        columnState = columnState,
+        modifier = modifier.padding(horizontal = 10.dp)
+    ) {
+        item {
+            DialogButtonsRow(
+                onCancelClick = onCancelClick,
+                onOKClick = onOKClick,
+                modifier = Modifier.padding(top = 10.dp)
+            )
+        }
+    }
+}
+
+@WearPreview
+@Composable
+fun SinglePasswordScreenPreview() {
+    SinglePasswordScreen(
+        email = "beckett_bakery@gmail.com",
+        onCancelClick = {},
+        onOKClick = {},
+        columnState = belowTimeTextPreview(),
+    )
+}
+
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreenViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreenViewModel.kt
new file mode 100644
index 0000000..9b06622
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreenViewModel.kt
@@ -0,0 +1,154 @@
+/*
+ * 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.0N
+ *
+ * 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.credentialmanager.ui.screens.single.password
+
+import android.content.Intent
+import android.credentials.ui.BaseDialogResult
+import android.credentials.ui.ProviderPendingIntentResponse
+import android.credentials.ui.UserSelectionDialogResult
+import android.os.Bundle
+import android.util.Log
+import androidx.activity.result.IntentSenderRequest
+import androidx.annotation.MainThread
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
+import androidx.lifecycle.viewModelScope
+import androidx.lifecycle.viewmodel.CreationExtras
+import com.android.credentialmanager.CredentialSelectorApp
+import com.android.credentialmanager.IS_AUTO_SELECTED_KEY
+import com.android.credentialmanager.TAG
+import com.android.credentialmanager.model.Password
+import com.android.credentialmanager.model.Request
+import com.android.credentialmanager.repository.RequestRepository
+import com.android.credentialmanager.ui.model.PasswordUiModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+
+class SinglePasswordScreenViewModel(
+    private val requestRepository: RequestRepository,
+) : ViewModel() {
+
+    private var initializeCalled = false
+
+    private lateinit var requestGet: Request.Get
+    private lateinit var password: Password
+
+    private val _uiState =
+        MutableStateFlow<SinglePasswordScreenUiState>(SinglePasswordScreenUiState.Idle)
+    val uiState: StateFlow<SinglePasswordScreenUiState> = _uiState
+
+    @MainThread
+    fun initialize() {
+        if (initializeCalled) return
+        initializeCalled = true
+
+        viewModelScope.launch {
+            val request = requestRepository.requests.first()
+            Log.d(TAG, "request: $request")
+
+            if (request !is Request.Get) {
+                _uiState.value = SinglePasswordScreenUiState.Error
+            } else {
+                requestGet = request
+                if (requestGet.passwordEntries.isEmpty()) {
+                    Log.d(TAG, "Empty passwordEntries")
+                    _uiState.value = SinglePasswordScreenUiState.Error
+                } else {
+                    password = requestGet.passwordEntries.first()
+                    _uiState.value = SinglePasswordScreenUiState.Loaded(
+                        PasswordUiModel(
+                            email = password.passwordCredentialEntry.username.toString(),
+                        )
+                    )
+                }
+            }
+        }
+    }
+
+    fun onCancelClick() {
+        _uiState.value = SinglePasswordScreenUiState.Cancel
+    }
+
+    fun onOKClick() {
+        // TODO: b/301206470 move this code to shared module
+        val entryIntent = password.entry.frameworkExtrasIntent
+        entryIntent?.putExtra(IS_AUTO_SELECTED_KEY, false)
+        val intentSenderRequest = IntentSenderRequest.Builder(
+            pendingIntent = password.passwordCredentialEntry.pendingIntent
+        ).setFillInIntent(entryIntent).build()
+
+        _uiState.value = SinglePasswordScreenUiState.PasswordSelected(
+            intentSenderRequest = intentSenderRequest
+        )
+    }
+
+    fun onPasswordInfoRetrieved(
+        resultCode: Int? = null,
+        resultData: Intent? = null,
+    ) {
+        // TODO: b/301206470 move this code to shared module
+        Log.d(TAG, "credential selected: {provider=${password.providerId}" +
+            ", key=${password.entry.key}, subkey=${password.entry.subkey}}")
+
+        val userSelectionDialogResult = UserSelectionDialogResult(
+            requestGet.token,
+            password.providerId,
+            password.entry.key,
+            password.entry.subkey,
+            if (resultCode != null) ProviderPendingIntentResponse(resultCode, resultData) else null
+        )
+        val resultDataBundle = Bundle()
+        UserSelectionDialogResult.addToBundle(userSelectionDialogResult, resultDataBundle)
+        requestGet.resultReceiver?.send(
+            BaseDialogResult.RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION,
+            resultDataBundle
+        )
+
+        _uiState.value = SinglePasswordScreenUiState.Completed
+    }
+
+    companion object {
+        val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
+            @Suppress("UNCHECKED_CAST")
+            override fun <T : ViewModel> create(
+                modelClass: Class<T>,
+                extras: CreationExtras
+            ): T {
+                val application = checkNotNull(extras[APPLICATION_KEY])
+
+                return SinglePasswordScreenViewModel(
+                    requestRepository = (application as CredentialSelectorApp).requestRepository,
+                ) as T
+            }
+        }
+    }
+}
+
+sealed class SinglePasswordScreenUiState {
+    data object Idle : SinglePasswordScreenUiState()
+    data class Loaded(val passwordUiModel: PasswordUiModel) : SinglePasswordScreenUiState()
+    data class PasswordSelected(
+        val intentSenderRequest: IntentSenderRequest
+    ) : SinglePasswordScreenUiState()
+
+    data object Cancel : SinglePasswordScreenUiState()
+    data object Error : SinglePasswordScreenUiState()
+    data object Completed : SinglePasswordScreenUiState()
+}
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 5e7e044..104f3d2 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -235,6 +235,9 @@
     srcs: [
         /* Status bar fakes */
         "tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/FakeAirplaneModeRepository.kt",
+        "tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt",
+        "tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt",
+        "tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt",
         "tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/FakeConnectivityRepository.kt",
         "tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt",
 
diff --git a/packages/SystemUI/shared/src/com/android/systemui/util/TraceUtils.kt b/packages/SystemUI/shared/src/com/android/systemui/util/TraceUtils.kt
index 96a974d..7b2e1af 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/util/TraceUtils.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/util/TraceUtils.kt
@@ -19,6 +19,12 @@
 import android.os.Trace
 import android.os.TraceNameSupplier
 import java.util.concurrent.atomic.AtomicInteger
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.async
 
 /**
  * Run a block within a [Trace] section. Calls [Trace.beginSection] before and [Trace.endSection]
@@ -85,5 +91,18 @@
                 Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, trackName, cookie)
             }
         }
+
+        /**
+         * Convenience method to avoid one indentation level when we want to add a trace when
+         * launching a coroutine
+         */
+        fun <T> CoroutineScope.tracedAsync(
+            method: String,
+            context: CoroutineContext = EmptyCoroutineContext,
+            start: CoroutineStart = CoroutineStart.DEFAULT,
+            block: suspend () -> T
+        ): Deferred<T> {
+            return async(context, start) { traceAsync(method) { block() } }
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 625c1de..b2287d87 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -163,7 +163,10 @@
             }
             mCurrentUser = KeyguardUpdateMonitor.getCurrentUser();
             showPrimarySecurityScreen(false);
-            reinflateViewFlipper((l) -> {});
+            if (mCurrentSecurityMode != SecurityMode.SimPin
+                    && mCurrentSecurityMode != SecurityMode.SimPuk) {
+                reinflateViewFlipper((l) -> {});
+            }
         }
     };
 
diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
index 954129e..22bd207 100644
--- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
@@ -18,8 +18,8 @@
 
 import static androidx.dynamicanimation.animation.DynamicAnimation.TRANSLATION_X;
 import static androidx.dynamicanimation.animation.FloatPropertyCompat.createFloatPropertyCompat;
-
 import static com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS;
+import static com.android.systemui.flags.Flags.SWIPE_UNCLEARED_TRANSIENT_VIEW_FIX;
 import static com.android.systemui.statusbar.notification.NotificationUtils.logKey;
 
 import android.animation.Animator;
@@ -482,7 +482,14 @@
                 boolean wasRemoved = false;
                 if (animView instanceof ExpandableNotificationRow) {
                     ExpandableNotificationRow row = (ExpandableNotificationRow) animView;
-                    wasRemoved = row.isRemoved();
+                    if (mFeatureFlags.isEnabled(SWIPE_UNCLEARED_TRANSIENT_VIEW_FIX)) {
+                        // If the view is already removed from its parent and added as Transient,
+                        // we need to clean the transient view upon animation end
+                        wasRemoved = row.getTransientContainer() != null
+                            || row.getParent() == null || row.isRemoved();
+                    } else {
+                        wasRemoved = row.isRemoved();
+                    }
                 }
                 if (!mCancelled || wasRemoved) {
                     mCallback.onChildDismissed(animView);
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
index 06cf723..e8740a4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
@@ -26,7 +26,6 @@
 import android.util.Log
 import com.android.internal.widget.LockPatternUtils
 import com.android.systemui.Dumpable
-import com.android.systemui.res.R
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.biometrics.data.repository.FacePropertyRepository
 import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
@@ -40,6 +39,8 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyguard.shared.model.AuthenticationFlags
 import com.android.systemui.keyguard.shared.model.DevicePosture
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
 import com.android.systemui.user.data.repository.UserRepository
 import java.io.PrintWriter
 import javax.inject.Inject
@@ -133,6 +134,7 @@
     devicePostureRepository: DevicePostureRepository,
     facePropertyRepository: FacePropertyRepository,
     fingerprintPropertyRepository: FingerprintPropertyRepository,
+    mobileConnectionsRepository: MobileConnectionsRepository,
     dumpManager: DumpManager,
 ) : BiometricSettingsRepository, Dumpable {
 
@@ -346,14 +348,15 @@
             .and(isFingerprintBiometricAllowed)
             .stateIn(scope, SharingStarted.Eagerly, false)
 
-    override val isFaceAuthEnrolledAndEnabled: Flow<Boolean>
-        get() = isFaceAuthenticationEnabled.and(isFaceEnrolled)
+    override val isFaceAuthEnrolledAndEnabled: Flow<Boolean> =
+        isFaceAuthenticationEnabled
+            .and(isFaceEnrolled)
+            .and(mobileConnectionsRepository.isAnySimSecure.isFalse())
 
-    override val isFaceAuthCurrentlyAllowed: Flow<Boolean>
-        get() =
-            isFaceAuthEnrolledAndEnabled
-                .and(isFaceBiometricsAllowed)
-                .and(isFaceAuthSupportedInCurrentPosture)
+    override val isFaceAuthCurrentlyAllowed: Flow<Boolean> =
+        isFaceAuthEnrolledAndEnabled
+            .and(isFaceBiometricsAllowed)
+            .and(isFaceAuthSupportedInCurrentPosture)
 }
 
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -426,3 +429,5 @@
 
 private fun Flow<Boolean>.and(anotherFlow: Flow<Boolean>): Flow<Boolean> =
     this.combine(anotherFlow) { a, b -> a && b }
+
+private fun Flow<Boolean>.isFalse(): Flow<Boolean> = this.map { !it }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
index dd7eee9..abc30ef 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
@@ -25,6 +25,8 @@
 import android.view.ViewGroup
 import android.view.ViewPropertyAnimator
 import android.widget.ImageView
+import androidx.core.animation.CycleInterpolator
+import androidx.core.animation.ObjectAnimator
 import androidx.core.view.isInvisible
 import androidx.core.view.isVisible
 import androidx.core.view.updateLayoutParams
@@ -383,6 +385,27 @@
                     falsingManager,
                 )
                 view.setOnTouchListener(onTouchListener)
+                view.setOnClickListener {
+                    messageDisplayer.invoke(R.string.keyguard_affordance_press_too_short)
+                    val amplitude =
+                        view.context.resources
+                            .getDimensionPixelSize(R.dimen.keyguard_affordance_shake_amplitude)
+                            .toFloat()
+                    val shakeAnimator =
+                        ObjectAnimator.ofFloat(
+                            view,
+                            "translationX",
+                            -amplitude / 2,
+                            amplitude / 2,
+                        )
+                    shakeAnimator.duration =
+                        KeyguardBottomAreaVibrations.ShakeAnimationDuration.inWholeMilliseconds
+                    shakeAnimator.interpolator =
+                        CycleInterpolator(KeyguardBottomAreaVibrations.ShakeAnimationCycles)
+                    shakeAnimator.start()
+
+                    vibratorHelper?.vibrate(KeyguardBottomAreaVibrations.Shake)
+                }
                 view.onLongClickListener =
                     OnLongClickListener(falsingManager, viewModel, vibratorHelper, onTouchListener)
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceOnTouchListener.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceOnTouchListener.kt
index 125e2da..f2d39da 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceOnTouchListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceOnTouchListener.kt
@@ -99,41 +99,7 @@
                     // When not using a stylus, lifting the finger/pointer will actually cancel
                     // the long-press gesture. Calling cancel after the quick affordance was
                     // already long-press activated is a no-op, so it's safe to call from here.
-                    cancel(
-                        onAnimationEnd =
-                            if (event.eventTime - event.downTime < longPressDurationMs) {
-                                Runnable {
-                                    messageDisplayer.invoke(
-                                        R.string.keyguard_affordance_press_too_short
-                                    )
-                                    val amplitude =
-                                        view.context.resources
-                                            .getDimensionPixelSize(
-                                                R.dimen.keyguard_affordance_shake_amplitude
-                                            )
-                                            .toFloat()
-                                    val shakeAnimator =
-                                        ObjectAnimator.ofFloat(
-                                            view,
-                                            "translationX",
-                                            -amplitude / 2,
-                                            amplitude / 2,
-                                        )
-                                    shakeAnimator.duration =
-                                        KeyguardBottomAreaVibrations.ShakeAnimationDuration
-                                            .inWholeMilliseconds
-                                    shakeAnimator.interpolator =
-                                        CycleInterpolator(
-                                            KeyguardBottomAreaVibrations.ShakeAnimationCycles
-                                        )
-                                    shakeAnimator.start()
-
-                                    vibratorHelper?.vibrate(KeyguardBottomAreaVibrations.Shake)
-                                }
-                            } else {
-                                null
-                            }
-                    )
+                    cancel()
                 }
                 false
             }
@@ -168,10 +134,10 @@
         view.setOnClickListener(null)
     }
 
-    fun cancel(onAnimationEnd: Runnable? = null) {
+    fun cancel() {
         longPressAnimator?.cancel()
         longPressAnimator = null
-        view.animate().scaleX(1f).scaleY(1f).withEndAction(onAnimationEnd)
+        view.animate().scaleX(1f).scaleY(1f)
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt
index eeb4ac3..aa76702 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt
@@ -23,6 +23,8 @@
 import android.view.View
 import android.view.ViewGroup
 import android.widget.ImageView
+import androidx.core.animation.CycleInterpolator
+import androidx.core.animation.ObjectAnimator
 import androidx.core.view.isInvisible
 import androidx.core.view.isVisible
 import androidx.core.view.updateLayoutParams
@@ -216,6 +218,27 @@
                     falsingManager,
                 )
                 view.setOnTouchListener(onTouchListener)
+                view.setOnClickListener {
+                    messageDisplayer.invoke(R.string.keyguard_affordance_press_too_short)
+                    val amplitude =
+                        view.context.resources
+                            .getDimensionPixelSize(R.dimen.keyguard_affordance_shake_amplitude)
+                            .toFloat()
+                    val shakeAnimator =
+                        ObjectAnimator.ofFloat(
+                            view,
+                            "translationX",
+                            -amplitude / 2,
+                            amplitude / 2,
+                        )
+                    shakeAnimator.duration =
+                        KeyguardBottomAreaVibrations.ShakeAnimationDuration.inWholeMilliseconds
+                    shakeAnimator.interpolator =
+                        CycleInterpolator(KeyguardBottomAreaVibrations.ShakeAnimationCycles)
+                    shakeAnimator.start()
+
+                    vibratorHelper?.vibrate(KeyguardBottomAreaVibrations.Shake)
+                }
                 view.onLongClickListener =
                     OnLongClickListener(falsingManager, viewModel, vibratorHelper, onTouchListener)
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index 6c2ce7f..1943b34 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -29,6 +29,7 @@
 import com.android.systemui.log.LogcatEchoTrackerProd;
 import com.android.systemui.log.table.TableLogBuffer;
 import com.android.systemui.log.table.TableLogBufferFactory;
+import com.android.systemui.qs.QSFragmentLegacy;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.util.Compile;
 import com.android.systemui.util.wakelock.WakeLockLog;
@@ -229,12 +230,12 @@
     }
 
     /**
-     * Provides a logging buffer for logs related to {@link com.android.systemui.qs.QSFragment}'s
+     * Provides a logging buffer for logs related to {@link QSFragmentLegacy}'s
      * disable flag adjustments.
      */
     @Provides
     @SysUISingleton
-    @QSFragmentDisableLog
+    @QSDisableLog
     public static LogBuffer provideQSFragmentDisableLogBuffer(LogBufferFactory factory) {
         return factory.create("QSFragmentDisableFlagsLog", 10 /* maxSize */,
                 false /* systrace */);
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/QSDisableLog.java
similarity index 89%
rename from packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java
rename to packages/SystemUI/src/com/android/systemui/log/dagger/QSDisableLog.java
index 557a254..b3bceca 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/QSDisableLog.java
@@ -19,6 +19,7 @@
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
 import com.android.systemui.log.LogBuffer;
+import com.android.systemui.qs.QSFragmentLegacy;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
@@ -27,10 +28,10 @@
 
 /**
  * A {@link LogBuffer} for disable flag adjustments made in
- * {@link com.android.systemui.qs.QSFragment}.
+ * {@link QSFragmentLegacy}.
  */
 @Qualifier
 @Documented
 @Retention(RUNTIME)
-public @interface QSFragmentDisableLog {
+public @interface QSDisableLog {
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
index 463c79c..eba1c25 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
@@ -28,7 +28,7 @@
 
 import com.android.app.animation.Interpolators;
 import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.dagger.qualifiers.RootView;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.plugins.qs.QSTileView;
 import com.android.systemui.qs.QSPanel.QSTileLayout;
@@ -86,8 +86,7 @@
     private final QuickQSPanel mQuickQsPanel;
     private final QSPanelController mQsPanelController;
     private final QuickQSPanelController mQuickQSPanelController;
-    private final QuickStatusBarHeader mQuickStatusBarHeader;
-    private final QS mQs;
+    private final View mQsRootView;
 
     @Nullable
     private PagedTileLayout mPagedLayout;
@@ -115,8 +114,6 @@
     // Brightness slider opacity driver. Uses linear interpolator.
     @Nullable
     private TouchAnimator mBrightnessOpacityAnimator;
-    // Animator for Footer actions in QQS
-    private TouchAnimator mQQSFooterActionsAnimator;
     // Height animator for QQS tiles (height changing from QQS size to QS size)
     @Nullable
     private HeightExpansionAnimator mQQSTileHeightAnimator;
@@ -144,22 +141,21 @@
     private int[] mTmpLoc2 = new int[2];
 
     @Inject
-    public QSAnimator(QS qs, QuickQSPanel quickPanel, QuickStatusBarHeader quickStatusBarHeader,
+    public QSAnimator(@RootView View rootView, QuickQSPanel quickPanel,
             QSPanelController qsPanelController,
             QuickQSPanelController quickQSPanelController, QSHost qsTileHost,
             @Main Executor executor, TunerService tunerService,
             QSExpansionPathInterpolator qsExpansionPathInterpolator) {
-        mQs = qs;
+        mQsRootView = rootView;
         mQuickQsPanel = quickPanel;
         mQsPanelController = qsPanelController;
         mQuickQSPanelController = quickQSPanelController;
-        mQuickStatusBarHeader = quickStatusBarHeader;
         mHost = qsTileHost;
         mExecutor = executor;
         mQSExpansionPathInterpolator = qsExpansionPathInterpolator;
         mHost.addCallback(this);
         mQsPanelController.addOnAttachStateChangeListener(this);
-        qs.getView().addOnLayoutChangeListener(this);
+        mQsRootView.addOnLayoutChangeListener(this);
         if (mQsPanelController.isAttachedToWindow()) {
             onViewAttachedToWindow(null);
         }
@@ -314,8 +310,7 @@
                     break;
                 }
 
-                final View tileIcon = tileView.getIcon().getIconView();
-                View view = mQs.getView();
+                View view = mQsRootView;
 
                 // This case: less tiles to animate in small displays.
                 if (count < mQuickQSPanelController.getTileLayout().getNumVisibleTiles()) {
@@ -480,7 +475,7 @@
                 .setStartDelay(QS_TILE_LABEL_FADE_OUT_START)
                 .setEndDelay(QS_TILE_LABEL_FADE_OUT_END);
         SideLabelTileLayout qqsLayout = (SideLabelTileLayout) mQuickQsPanel.getTileLayout();
-        View view = mQs.getView();
+        View view = mQsRootView;
         List<String> specs = mPagedLayout.getSpecsForPage(page);
         if (specs.isEmpty()) {
             // specs should not be empty in a valid secondary page, as we scrolled to it.
@@ -577,7 +572,7 @@
 
             // For (1), compute the distance via the vertical distance between QQS and QS tile
             // layout top.
-            View quickSettingsRootView = mQs.getView();
+            View quickSettingsRootView = mQsRootView;
             View qsTileLayout = (View) mQsPanelController.getTileLayout();
             View qqsTileLayout = (View) mQuickQSPanelController.getTileLayout();
             getRelativePosition(mTmpLoc1, qsTileLayout, quickSettingsRootView);
@@ -607,7 +602,7 @@
     private int getRelativeTranslationY(View view1, View view2) {
         int[] qsPosition = new int[2];
         int[] qqsPosition = new int[2];
-        View commonView = mQs.getView();
+        View commonView = mQsRootView;
         getRelativePositionInt(qsPosition, view1, commonView);
         getRelativePositionInt(qqsPosition, view2, commonView);
         return qsPosition[1] - qqsPosition[1];
@@ -690,9 +685,6 @@
         if (mBrightnessTranslationAnimator != null) {
             mBrightnessTranslationAnimator.setPosition(position);
         }
-        if (mQQSFooterActionsAnimator != null) {
-            mQQSFooterActionsAnimator.setPosition(position);
-        }
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/QSDisableFlagsLogger.kt
similarity index 73%
rename from packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt
rename to packages/SystemUI/src/com/android/systemui/qs/QSDisableFlagsLogger.kt
index 6563e42..6f6f467 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSDisableFlagsLogger.kt
@@ -1,20 +1,22 @@
 package com.android.systemui.qs
 
-import com.android.systemui.log.dagger.QSFragmentDisableLog
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.QSDisableLog
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger
 import javax.inject.Inject
 
-/** A helper class for logging disable flag changes made in [QSFragment]. */
-class QSFragmentDisableFlagsLogger @Inject constructor(
-    @QSFragmentDisableLog private val buffer: LogBuffer,
+/** A helper class for logging disable flag changes made in [QSImpl]. */
+class QSDisableFlagsLogger
+@Inject
+constructor(
+    @QSDisableLog private val buffer: LogBuffer,
     private val disableFlagsLogger: DisableFlagsLogger
 ) {
 
     /**
-     * Logs a string representing the new state received by [QSFragment] and any modifications that
-     * were made to the flags locally.
+     * Logs a string representing the new state received by [QSImpl] and any modifications that were
+     * made to the flags locally.
      *
      * @param new see [DisableFlagsLogger.getDisableFlagsString]
      * @param newAfterLocalModification see [DisableFlagsLogger.getDisableFlagsString]
@@ -43,4 +45,4 @@
     }
 }
 
-private const val TAG = "QSFragmentDisableFlagsLog"
+private const val TAG = "QSDisableFlagsLog"
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentLegacy.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentLegacy.java
new file mode 100644
index 0000000..8589ae9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentLegacy.java
@@ -0,0 +1,369 @@
+/*
+ * 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.qs;
+
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.Trace;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.FloatRange;
+import androidx.annotation.Nullable;
+
+import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.plugins.qs.QSContainerController;
+import com.android.systemui.qs.dagger.QSFragmentComponent;
+import com.android.systemui.res.R;
+import com.android.systemui.statusbar.policy.BrightnessMirrorController;
+import com.android.systemui.util.LifecycleFragment;
+
+import java.util.function.Consumer;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+
+public class QSFragmentLegacy extends LifecycleFragment implements QS {
+
+    private final Provider<QSImpl> mQsImplProvider;
+
+    private final QSFragmentComponent.Factory mQsComponentFactory;
+
+    @Nullable
+    private QSImpl mQsImpl;
+
+    @Inject
+    public QSFragmentLegacy(
+            Provider<QSImpl> qsImplProvider,
+            QSFragmentComponent.Factory qsComponentFactory
+    ) {
+        mQsComponentFactory = qsComponentFactory;
+        mQsImplProvider = qsImplProvider;
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+            Bundle savedInstanceState) {
+        try {
+            Trace.beginSection("QSFragment#onCreateView");
+            inflater = inflater.cloneInContext(new ContextThemeWrapper(getContext(),
+                    R.style.Theme_SystemUI_QuickSettings));
+            return inflater.inflate(R.layout.qs_panel, container, false);
+        } finally {
+            Trace.endSection();
+        }
+    }
+
+    @Override
+    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+        QSFragmentComponent qsFragmentComponent = mQsComponentFactory.create(this);
+        mQsImpl = mQsImplProvider.get();
+        mQsImpl.onComponentCreated(qsFragmentComponent, savedInstanceState);
+    }
+
+    @Override
+    public void setScrollListener(ScrollListener listener) {
+        if (mQsImpl != null) {
+            mQsImpl.setScrollListener(listener);
+        }
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        if (mQsImpl != null) {
+            mQsImpl.onCreate(savedInstanceState);
+        }
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        if (mQsImpl != null) {
+            mQsImpl.onDestroy();
+        }
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        if (mQsImpl != null) {
+            mQsImpl.onSaveInstanceState(outState);
+        }
+    }
+
+    @Override
+    public View getHeader() {
+        if (mQsImpl != null) {
+            return mQsImpl.getHeader();
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public void setHasNotifications(boolean hasNotifications) {
+        if (mQsImpl != null) {
+            mQsImpl.setHasNotifications(hasNotifications);
+        }
+    }
+
+    @Override
+    public void setPanelView(HeightListener panelView) {
+        if (mQsImpl != null) {
+            mQsImpl.setPanelView(panelView);
+        }
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        if (mQsImpl != null) {
+            mQsImpl.onConfigurationChanged(newConfig);
+        }
+    }
+
+    @Override
+    public void setFancyClipping(int leftInset, int top, int rightInset, int bottom,
+            int cornerRadius, boolean visible, boolean fullWidth) {
+        if (mQsImpl != null) {
+            mQsImpl.setFancyClipping(leftInset, top, rightInset, bottom, cornerRadius, visible,
+                    fullWidth);
+        }
+    }
+
+    @Override
+    public boolean isFullyCollapsed() {
+        if (mQsImpl != null) {
+            return mQsImpl.isFullyCollapsed();
+        } else {
+            return true;
+        }
+    }
+
+    @Override
+    public void setCollapsedMediaVisibilityChangedListener(Consumer<Boolean> listener) {
+        if (mQsImpl != null) {
+            mQsImpl.setCollapsedMediaVisibilityChangedListener(listener);
+        }
+    }
+
+    @Override
+    public void setContainerController(QSContainerController controller) {
+        if (mQsImpl != null) {
+            mQsImpl.setContainerController(controller);
+        }
+    }
+
+    @Override
+    public boolean isCustomizing() {
+        if (mQsImpl != null) {
+            return mQsImpl.isCustomizing();
+        } else {
+            return false;
+        }
+    }
+
+    public QSPanelController getQSPanelController() {
+        if (mQsImpl != null) {
+            return mQsImpl.getQSPanelController();
+        } else {
+            return null;
+        }
+    }
+
+    public void setBrightnessMirrorController(
+            BrightnessMirrorController brightnessMirrorController) {
+        if (mQsImpl != null) {
+            mQsImpl.setBrightnessMirrorController(brightnessMirrorController);
+        }
+    }
+
+    @Override
+    public boolean isShowingDetail() {
+        if (mQsImpl != null) {
+            return mQsImpl.isShowingDetail();
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public void setHeaderClickable(boolean clickable) {
+        if (mQsImpl != null) {
+            mQsImpl.setHeaderClickable(clickable);
+        }
+    }
+
+    @Override
+    public void setExpanded(boolean expanded) {
+        if (mQsImpl != null) {
+            mQsImpl.setExpanded(expanded);
+        }
+    }
+
+    @Override
+    public void setOverscrolling(boolean stackScrollerOverscrolling) {
+        if (mQsImpl != null) {
+            mQsImpl.setOverscrolling(stackScrollerOverscrolling);
+        }
+    }
+
+    @Override
+    public void setListening(boolean listening) {
+        if (mQsImpl != null) {
+            mQsImpl.setListening(listening);
+        }
+    }
+
+    @Override
+    public void setQsVisible(boolean visible) {
+        if (mQsImpl != null) {
+            mQsImpl.setQsVisible(visible);
+        }
+    }
+
+    @Override
+    public void setHeaderListening(boolean listening) {
+        if (mQsImpl != null) {
+            mQsImpl.setHeaderListening(listening);
+        }
+    }
+
+    @Override
+    public void notifyCustomizeChanged() {
+        if (mQsImpl != null) {
+            mQsImpl.notifyCustomizeChanged();
+        }
+    }
+
+    @Override
+    public void setInSplitShade(boolean inSplitShade) {
+        if (mQsImpl != null) {
+            mQsImpl.setInSplitShade(inSplitShade);
+        }
+    }
+
+    @Override
+    public void setTransitionToFullShadeProgress(
+            boolean isTransitioningToFullShade,
+            @FloatRange(from = 0.0, to = 1.0) float qsTransitionFraction,
+            @FloatRange(from = 0.0, to = 1.0) float qsSquishinessFraction) {
+        if (mQsImpl != null) {
+            mQsImpl.setTransitionToFullShadeProgress(isTransitioningToFullShade,
+                    qsTransitionFraction, qsSquishinessFraction);
+        }
+    }
+
+    @Override
+    public void setOverScrollAmount(int overScrollAmount) {
+        if (mQsImpl != null) {
+            mQsImpl.setOverScrollAmount(overScrollAmount);
+        }
+    }
+
+    @Override
+    public int getHeightDiff() {
+        if (mQsImpl != null) {
+            return mQsImpl.getHeightDiff();
+        } else {
+            return 0;
+        }
+    }
+
+    @Override
+    public void setIsNotificationPanelFullWidth(boolean isFullWidth) {
+        if (mQsImpl != null) {
+            mQsImpl.setIsNotificationPanelFullWidth(isFullWidth);
+        }
+    }
+
+    @Override
+    public void setQsExpansion(float expansion, float panelExpansionFraction,
+            float proposedTranslation, float squishinessFraction) {
+        if (mQsImpl != null) {
+            mQsImpl.setQsExpansion(expansion, panelExpansionFraction, proposedTranslation,
+                    squishinessFraction);
+        }
+    }
+
+    @Override
+    public void animateHeaderSlidingOut() {
+        if (mQsImpl != null) {
+            mQsImpl.animateHeaderSlidingOut();
+        }
+    }
+
+    @Override
+    public void setCollapseExpandAction(Runnable action) {
+        if (mQsImpl != null) {
+            mQsImpl.setCollapseExpandAction(action);
+        }
+    }
+
+    @Override
+    public void closeDetail() {
+        if (mQsImpl != null) {
+            mQsImpl.closeDetail();
+        }
+    }
+
+    @Override
+    public void closeCustomizer() {
+        if (mQsImpl != null) {
+            mQsImpl.closeDetail();
+        }
+    }
+
+    /**
+     * The height this view wants to be. This is different from {@link View#getMeasuredHeight} such
+     * that during closing the detail panel, this already returns the smaller height.
+     */
+    @Override
+    public int getDesiredHeight() {
+        if (mQsImpl != null) {
+            return mQsImpl.getDesiredHeight();
+        } else {
+            return 0;
+        }
+    }
+
+    @Override
+    public void setHeightOverride(int desiredHeight) {
+        if (mQsImpl != null) {
+            mQsImpl.setHeightOverride(desiredHeight);
+        }
+    }
+
+    @Override
+    public int getQsMinExpansionHeight() {
+        if (mQsImpl != null) {
+            return mQsImpl.getQsMinExpansionHeight();
+        } else {
+            return 0;
+        }
+    }
+
+    @Override
+    public void hideImmediately() {
+        if (mQsImpl != null) {
+            mQsImpl.hideImmediately();
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentStartable.kt b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentStartable.kt
index 253560b..9fa6769 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentStartable.kt
@@ -31,10 +31,13 @@
 @Inject
 constructor(
     private val fragmentService: FragmentService,
-    private val qsFragmentProvider: Provider<QSFragment>
+    private val qsFragmentLegacyProvider: Provider<QSFragmentLegacy>
 ) : CoreStartable {
     override fun start() {
-        fragmentService.addFragmentInstantiationProvider(QSFragment::class.java, qsFragmentProvider)
+        fragmentService.addFragmentInstantiationProvider(
+            QSFragmentLegacy::class.java,
+            qsFragmentLegacyProvider
+        )
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
similarity index 90%
rename from packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
rename to packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
index fd81e9a..a32a024 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
@@ -1,15 +1,17 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * 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
+ * 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.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
  */
 
 package com.android.systemui.qs;
@@ -20,21 +22,18 @@
 import static com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL;
 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
 import static com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED;
-import static com.android.systemui.statusbar.disableflags.DisableFlagsLogger.DisableState;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
 import android.content.res.Configuration;
+import android.content.res.Resources;
 import android.graphics.Rect;
 import android.os.Bundle;
-import android.os.Trace;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
-import android.view.ContextThemeWrapper;
-import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
 import android.widget.LinearLayout;
 
 import androidx.annotation.FloatRange;
@@ -47,7 +46,6 @@
 import com.android.app.animation.Interpolators;
 import com.android.keyguard.BouncerPanelExpansionCalculator;
 import com.android.systemui.Dumpable;
-import com.android.systemui.res.R;
 import com.android.systemui.animation.ShadeInterpolation;
 import com.android.systemui.compose.ComposeFacade;
 import com.android.systemui.dump.DumpManager;
@@ -58,19 +56,20 @@
 import com.android.systemui.plugins.qs.QSContainerController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.qs.customize.QSCustomizerController;
-import com.android.systemui.qs.dagger.QSFragmentComponent;
+import com.android.systemui.qs.dagger.QSComponent;
 import com.android.systemui.qs.footer.ui.binder.FooterActionsViewBinder;
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel;
 import com.android.systemui.qs.logging.QSLogger;
+import com.android.systemui.res.R;
 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
+import com.android.systemui.statusbar.disableflags.DisableFlagsLogger;
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.policy.BrightnessMirrorController;
 import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
-import com.android.systemui.util.LifecycleFragment;
 import com.android.systemui.util.Utils;
 
 import java.io.PrintWriter;
@@ -80,8 +79,8 @@
 import javax.inject.Inject;
 import javax.inject.Named;
 
-public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Callbacks,
-        StatusBarStateController.StateListener, Dumpable {
+public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateController.StateListener,
+        Dumpable {
     private static final String TAG = "QS";
     private static final boolean DEBUG = false;
     private static final String EXTRA_EXPANDED = "expanded";
@@ -113,8 +112,7 @@
     private final RemoteInputQuickSettingsDisabler mRemoteInputQuickSettingsDisabler;
     private final MediaHost mQsMediaHost;
     private final MediaHost mQqsMediaHost;
-    private final QSFragmentComponent.Factory mQsComponentFactory;
-    private final QSFragmentDisableFlagsLogger mQsFragmentDisableFlagsLogger;
+    private final QSDisableFlagsLogger mQsDisableFlagsLogger;
     private final LargeScreenShadeInterpolator mLargeScreenShadeInterpolator;
     private final FeatureFlags mFeatureFlags;
     private final QSLogger mLogger;
@@ -167,14 +165,17 @@
 
     private boolean mIsSmallScreen;
 
+    private CommandQueue mCommandQueue;
+
+    private View mRootView;
+
     @Inject
-    public QSFragment(RemoteInputQuickSettingsDisabler remoteInputQsDisabler,
+    public QSImpl(RemoteInputQuickSettingsDisabler remoteInputQsDisabler,
             SysuiStatusBarStateController statusBarStateController, CommandQueue commandQueue,
             @Named(QS_PANEL) MediaHost qsMediaHost,
             @Named(QUICK_QS_PANEL) MediaHost qqsMediaHost,
             KeyguardBypassController keyguardBypassController,
-            QSFragmentComponent.Factory qsComponentFactory,
-            QSFragmentDisableFlagsLogger qsFragmentDisableFlagsLogger,
+            QSDisableFlagsLogger qsDisableFlagsLogger,
             DumpManager dumpManager, QSLogger qsLogger,
             FooterActionsController footerActionsController,
             FooterActionsViewModel.Factory footerActionsViewModelFactory,
@@ -184,12 +185,11 @@
         mRemoteInputQuickSettingsDisabler = remoteInputQsDisabler;
         mQsMediaHost = qsMediaHost;
         mQqsMediaHost = qqsMediaHost;
-        mQsComponentFactory = qsComponentFactory;
-        mQsFragmentDisableFlagsLogger = qsFragmentDisableFlagsLogger;
+        mQsDisableFlagsLogger = qsDisableFlagsLogger;
         mLogger = qsLogger;
         mLargeScreenShadeInterpolator = largeScreenShadeInterpolator;
         mFeatureFlags = featureFlags;
-        commandQueue.observe(getLifecycle(), this);
+        mCommandQueue = commandQueue;
         mBypassController = keyguardBypassController;
         mStatusBarStateController = statusBarStateController;
         mDumpManager = dumpManager;
@@ -199,34 +199,23 @@
         mListeningAndVisibilityLifecycleOwner = new ListeningAndVisibilityLifecycleOwner();
     }
 
-    @Override
-    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
-            Bundle savedInstanceState) {
-        try {
-            Trace.beginSection("QSFragment#onCreateView");
-            inflater = inflater.cloneInContext(new ContextThemeWrapper(getContext(),
-                    R.style.Theme_SystemUI_QuickSettings));
-            return inflater.inflate(R.layout.qs_panel, container, false);
-        } finally {
-            Trace.endSection();
-        }
-    }
+    public void onComponentCreated(QSComponent qsComponent, @Nullable Bundle savedInstanceState) {
+        mRootView = qsComponent.getRootView();
 
-    @Override
-    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
-        QSFragmentComponent qsFragmentComponent = mQsComponentFactory.create(this);
-        mQSPanelController = qsFragmentComponent.getQSPanelController();
-        mQuickQSPanelController = qsFragmentComponent.getQuickQSPanelController();
+        mCommandQueue.addCallback(this);
+
+        mQSPanelController = qsComponent.getQSPanelController();
+        mQuickQSPanelController = qsComponent.getQuickQSPanelController();
 
         mQSPanelController.init();
         mQuickQSPanelController.init();
 
-        mQSFooterActionsViewModel = mFooterActionsViewModelFactory.create(/* lifecycleOwner */
-                this);
-        bindFooterActionsView(view);
+        mQSFooterActionsViewModel = mFooterActionsViewModelFactory
+                .create(mListeningAndVisibilityLifecycleOwner);
+        bindFooterActionsView(mRootView);
         mFooterActionsController.init();
 
-        mQSPanelScrollView = view.findViewById(R.id.expanded_qs_scroll_view);
+        mQSPanelScrollView = mRootView.findViewById(R.id.expanded_qs_scroll_view);
         mQSPanelScrollView.addOnLayoutChangeListener(
                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
                     updateQsBounds();
@@ -238,26 +227,26 @@
                     if (mScrollListener != null) {
                         mScrollListener.onQsPanelScrollChanged(scrollY);
                     }
-        });
-        mHeader = view.findViewById(R.id.header);
-        mFooter = qsFragmentComponent.getQSFooter();
+                });
+        mHeader = mRootView.findViewById(R.id.header);
+        mFooter = qsComponent.getQSFooter();
 
-        mQSContainerImplController = qsFragmentComponent.getQSContainerImplController();
+        mQSContainerImplController = qsComponent.getQSContainerImplController();
         mQSContainerImplController.init();
         mContainer = mQSContainerImplController.getView();
         mDumpManager.registerDumpable(mContainer.getClass().getSimpleName(), mContainer);
 
-        mQSAnimator = qsFragmentComponent.getQSAnimator();
-        mQSSquishinessController = qsFragmentComponent.getQSSquishinessController();
+        mQSAnimator = qsComponent.getQSAnimator();
+        mQSSquishinessController = qsComponent.getQSSquishinessController();
 
-        mQSCustomizerController = qsFragmentComponent.getQSCustomizerController();
+        mQSCustomizerController = qsComponent.getQSCustomizerController();
         mQSCustomizerController.init();
         mQSCustomizerController.setQs(this);
         if (savedInstanceState != null) {
             setQsVisible(savedInstanceState.getBoolean(EXTRA_VISIBLE));
             setExpanded(savedInstanceState.getBoolean(EXTRA_EXPANDED));
             setListening(savedInstanceState.getBoolean(EXTRA_LISTENING));
-            setEditLocation(view);
+            setEditLocation(mRootView);
             mQSCustomizerController.restoreInstanceState(savedInstanceState);
             if (mQsExpanded) {
                 mQSPanelController.getTileLayout().restoreInstanceState(savedInstanceState);
@@ -265,7 +254,7 @@
         }
         mStatusBarStateController.addCallback(this);
         onStateChanged(mStatusBarStateController.getState());
-        view.addOnLayoutChangeListener(
+        mRootView.addOnLayoutChangeListener(
                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
                     boolean sizeChanged = (oldTop - oldBottom) != (top - bottom);
                     if (sizeChanged) {
@@ -327,15 +316,12 @@
         mScrollListener = listener;
     }
 
-    @Override
     public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
         mDumpManager.registerDumpable(getClass().getSimpleName(), this);
     }
 
-    @Override
     public void onDestroy() {
-        super.onDestroy();
+        mCommandQueue.removeCallback(this);
         mStatusBarStateController.removeCallback(this);
         if (mListening) {
             setListening(false);
@@ -351,9 +337,7 @@
         mListeningAndVisibilityLifecycleOwner.destroy();
     }
 
-    @Override
     public void onSaveInstanceState(Bundle outState) {
-        super.onSaveInstanceState(outState);
         outState.putBoolean(EXTRA_EXPANDED, mQsExpanded);
         outState.putBoolean(EXTRA_LISTENING, mListening);
         outState.putBoolean(EXTRA_VISIBLE, mQsVisible);
@@ -394,9 +378,7 @@
         mPanelView = panelView;
     }
 
-    @Override
     public void onConfigurationChanged(Configuration newConfig) {
-        super.onConfigurationChanged(newConfig);
         setEditLocation(getView());
         if (newConfig.getLayoutDirection() != mLayoutDirection) {
             mLayoutDirection = newConfig.getLayoutDirection();
@@ -452,9 +434,9 @@
         int state2BeforeAdjustment = state2;
         state2 = mRemoteInputQuickSettingsDisabler.adjustDisableFlags(state2);
 
-        mQsFragmentDisableFlagsLogger.logDisableFlagChange(
-                /* new= */ new DisableState(state1, state2BeforeAdjustment),
-                /* newAfterLocalModification= */ new DisableState(state1, state2)
+        mQsDisableFlagsLogger.logDisableFlagChange(
+                /* new= */ new DisableFlagsLogger.DisableState(state1, state2BeforeAdjustment),
+                /* newAfterLocalModification= */ new DisableFlagsLogger.DisableState(state1, state2)
         );
 
         final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0;
@@ -919,32 +901,6 @@
         getView().setY(-getQsMinExpansionHeight());
     }
 
-    private final ViewTreeObserver.OnPreDrawListener mStartHeaderSlidingIn
-            = new ViewTreeObserver.OnPreDrawListener() {
-        @Override
-        public boolean onPreDraw() {
-            getView().getViewTreeObserver().removeOnPreDrawListener(this);
-            getView().animate()
-                    .translationY(0f)
-                    .setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE)
-                    .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
-                    .setListener(mAnimateHeaderSlidingInListener)
-                    .start();
-            return true;
-        }
-    };
-
-    private final Animator.AnimatorListener mAnimateHeaderSlidingInListener
-            = new AnimatorListenerAdapter() {
-        @Override
-        public void onAnimationEnd(Animator animation) {
-            mHeaderAnimating = false;
-            updateQsState();
-            // Unset the listener, otherwise this may persist for another view property animation
-            getView().animate().setListener(null);
-        }
-    };
-
     @Override
     public void onUpcomingStateChanged(int upcomingState) {
         if (upcomingState == KEYGUARD) {
@@ -1030,6 +986,20 @@
         return "GONE";
     }
 
+    @Override
+    public View getView() {
+        return mRootView;
+    }
+
+    @Override
+    public Context getContext() {
+        return mRootView.getContext();
+    }
+
+    private Resources getResources() {
+        return getContext().getResources();
+    }
+
     /**
      * A {@link LifecycleOwner} whose state is driven by the current state of this fragment:
      *
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
index 9359958..6bbdc54 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
@@ -19,7 +19,7 @@
 import static com.android.systemui.classifier.Classifier.QS_SWIPE_SIDE;
 import static com.android.systemui.media.dagger.MediaModule.QS_PANEL;
 import static com.android.systemui.qs.QSPanel.QS_SHOW_BRIGHTNESS;
-import static com.android.systemui.qs.dagger.QSFragmentModule.QS_USING_MEDIA_PLAYER;
+import static com.android.systemui.qs.dagger.QSScopeModule.QS_USING_MEDIA_PLAYER;
 
 import android.view.MotionEvent;
 import android.view.View;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index ef81674..60c92c0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -17,7 +17,6 @@
 package com.android.systemui.qs;
 
 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import static com.android.systemui.qs.dagger.QSFragmentModule.QS_USING_MEDIA_PLAYER;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -44,9 +43,6 @@
 import com.android.systemui.util.ViewController;
 import com.android.systemui.util.animation.DisappearParameters;
 
-import kotlin.Unit;
-import kotlin.jvm.functions.Function1;
-
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -54,7 +50,8 @@
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
-import javax.inject.Named;
+import kotlin.Unit;
+import kotlin.jvm.functions.Function1;
 
 /**
  * Controller for QSPanel views.
@@ -135,7 +132,7 @@
             T view,
             QSHost host,
             QSCustomizerController qsCustomizerController,
-            @Named(QS_USING_MEDIA_PLAYER) boolean usingMediaPlayer,
+            boolean usingMediaPlayer,
             MediaHost mediaHost,
             MetricsLogger metricsLogger,
             UiEventLogger uiEventLogger,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
index 099d19d8..f278dce 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
@@ -17,14 +17,13 @@
 package com.android.systemui.qs;
 
 import static com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL;
-import static com.android.systemui.qs.dagger.QSFragmentModule.QS_USING_COLLAPSED_LANDSCAPE_MEDIA;
-import static com.android.systemui.qs.dagger.QSFragmentModule.QS_USING_MEDIA_PLAYER;
+import static com.android.systemui.qs.dagger.QSScopeModule.QS_USING_COLLAPSED_LANDSCAPE_MEDIA;
+import static com.android.systemui.qs.dagger.QSScopeModule.QS_USING_MEDIA_PLAYER;
 
 import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.UiEventLogger;
-import com.android.systemui.res.R;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.media.controls.ui.MediaHierarchyManager;
 import com.android.systemui.media.controls.ui.MediaHost;
@@ -32,6 +31,7 @@
 import com.android.systemui.qs.customize.QSCustomizerController;
 import com.android.systemui.qs.dagger.QSScope;
 import com.android.systemui.qs.logging.QSLogger;
+import com.android.systemui.res.R;
 import com.android.systemui.statusbar.policy.SplitShadeStateController;
 import com.android.systemui.util.leak.RotationUtils;
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
index 7888f4c..a103566 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
@@ -33,11 +33,11 @@
 import androidx.recyclerview.widget.DefaultItemAnimator;
 import androidx.recyclerview.widget.RecyclerView;
 
-import com.android.systemui.res.R;
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.plugins.qs.QSContainerController;
 import com.android.systemui.qs.QSDetailClipper;
 import com.android.systemui.qs.QSUtils;
+import com.android.systemui.res.R;
 import com.android.systemui.statusbar.phone.LightBarController;
 
 /**
@@ -135,8 +135,10 @@
             setVisibility(View.VISIBLE);
             long duration = mClipper.animateCircularClip(
                     mX, mY, true, new ExpandAnimatorListener(tileAdapter));
-            mQsContainerController.setCustomizerAnimating(true);
-            mQsContainerController.setCustomizerShowing(true, duration);
+            if (mQsContainerController != null) {
+                mQsContainerController.setCustomizerAnimating(true);
+                mQsContainerController.setCustomizerShowing(true, duration);
+            }
         }
     }
 
@@ -150,8 +152,10 @@
             mClipper.showBackground();
             isShown = true;
             setCustomizing(true);
-            mQsContainerController.setCustomizerAnimating(false);
-            mQsContainerController.setCustomizerShowing(true);
+            if (mQsContainerController != null) {
+                mQsContainerController.setCustomizerAnimating(false);
+                mQsContainerController.setCustomizerShowing(true);
+            }
         }
     }
 
@@ -169,8 +173,10 @@
             } else {
                 setVisibility(View.GONE);
             }
-            mQsContainerController.setCustomizerAnimating(animate);
-            mQsContainerController.setCustomizerShowing(false, duration);
+            if (mQsContainerController != null) {
+                mQsContainerController.setCustomizerAnimating(animate);
+                mQsContainerController.setCustomizerShowing(false, duration);
+            }
         }
     }
 
@@ -180,7 +186,9 @@
 
     void setCustomizing(boolean customizing) {
         mCustomizing = customizing;
-        mQs.notifyCustomizeChanged();
+        if (mQs != null) {
+            mQs.notifyCustomizeChanged();
+        }
     }
 
     public boolean isCustomizing() {
@@ -208,15 +216,21 @@
                 setCustomizing(true);
             }
             mOpening = false;
-            mQsContainerController.setCustomizerAnimating(false);
+            if (mQsContainerController != null) {
+                mQsContainerController.setCustomizerAnimating(false);
+            }
             mRecyclerView.setAdapter(mTileAdapter);
         }
 
         @Override
         public void onAnimationCancel(Animator animation) {
             mOpening = false;
-            mQs.notifyCustomizeChanged();
-            mQsContainerController.setCustomizerAnimating(false);
+            if (mQs != null) {
+                mQs.notifyCustomizeChanged();
+            }
+            if (mQsContainerController != null) {
+                mQsContainerController.setCustomizerAnimating(false);
+            }
         }
     }
 
@@ -226,7 +240,9 @@
             if (!isShown) {
                 setVisibility(View.GONE);
             }
-            mQsContainerController.setCustomizerAnimating(false);
+            if (mQsContainerController != null) {
+                mQsContainerController.setCustomizerAnimating(false);
+            }
         }
 
         @Override
@@ -234,7 +250,9 @@
             if (!isShown) {
                 setVisibility(View.GONE);
             }
-            mQsContainerController.setCustomizerAnimating(false);
+            if (mQsContainerController != null) {
+                mQsContainerController.setCustomizerAnimating(false);
+            }
         }
     };
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java
index ce504b2..024e760 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java
@@ -34,14 +34,14 @@
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.internal.logging.UiEventLogger;
-import com.android.systemui.res.R;
 import com.android.systemui.keyguard.ScreenLifecycle;
+import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.plugins.qs.QSContainerController;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.qs.QSEditEvent;
-import com.android.systemui.qs.QSFragment;
 import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.dagger.QSScope;
+import com.android.systemui.res.R;
 import com.android.systemui.statusbar.phone.LightBarController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
@@ -199,7 +199,7 @@
     }
 
     /** */
-    public void setQs(@Nullable QSFragment qsFragment) {
+    public void setQs(@Nullable QS qsFragment) {
         mView.setQs(qsFragment);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSComponent.kt b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSComponent.kt
new file mode 100644
index 0000000..f3413b80
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSComponent.kt
@@ -0,0 +1,40 @@
+package com.android.systemui.qs.dagger
+
+import android.view.View
+import com.android.systemui.dagger.qualifiers.RootView
+import com.android.systemui.qs.FooterActionsController
+import com.android.systemui.qs.QSAnimator
+import com.android.systemui.qs.QSContainerImplController
+import com.android.systemui.qs.QSFooter
+import com.android.systemui.qs.QSPanelController
+import com.android.systemui.qs.QSSquishinessController
+import com.android.systemui.qs.QuickQSPanelController
+import com.android.systemui.qs.customize.QSCustomizerController
+
+interface QSComponent {
+    /** Construct a [QSPanelController]. */
+    fun getQSPanelController(): QSPanelController
+
+    /** Construct a [QuickQSPanelController]. */
+    fun getQuickQSPanelController(): QuickQSPanelController
+
+    /** Construct a [QSAnimator]. */
+    fun getQSAnimator(): QSAnimator
+
+    /** Construct a [QSContainerImplController]. */
+    fun getQSContainerImplController(): QSContainerImplController
+
+    /** Construct a [QSFooter] */
+    fun getQSFooter(): QSFooter
+
+    /** Construct a [QSCustomizerController]. */
+    fun getQSCustomizerController(): QSCustomizerController
+
+    /** Construct a [QSSquishinessController]. */
+    fun getQSSquishinessController(): QSSquishinessController
+
+    /** Construct a [FooterActionsController]. */
+    fun getQSFooterActionController(): FooterActionsController
+
+    @RootView fun getRootView(): View
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFlexiglassComponent.kt b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFlexiglassComponent.kt
new file mode 100644
index 0000000..ba1aa62
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFlexiglassComponent.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.qs.dagger
+
+import android.view.View
+import com.android.systemui.dagger.qualifiers.RootView
+import dagger.BindsInstance
+import dagger.Subcomponent
+
+@Subcomponent(modules = [QSFlexiglassModule::class])
+@QSScope
+interface QSFlexiglassComponent : QSComponent {
+
+    @Subcomponent.Factory
+    interface Factory {
+        fun create(@RootView @BindsInstance rootView: View): QSFlexiglassComponent
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFlexiglassModule.kt b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFlexiglassModule.kt
new file mode 100644
index 0000000..36fac44
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFlexiglassModule.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.qs.dagger
+
+import android.content.Context
+import com.android.systemui.qs.dagger.QSScopeModule.Companion.QS_USING_COLLAPSED_LANDSCAPE_MEDIA
+import com.android.systemui.qs.dagger.QSScopeModule.Companion.QS_USING_MEDIA_PLAYER
+import dagger.Module
+import dagger.Provides
+import javax.inject.Named
+
+@Module(includes = [QSScopeModule::class])
+interface QSFlexiglassModule {
+
+    @Module
+    companion object {
+
+        /**  */
+        @Provides
+        @Named(QS_USING_MEDIA_PLAYER)
+        @JvmStatic
+        fun providesQSUsingMediaPlayer(context: Context?): Boolean {
+            return false
+        }
+
+        /**  */
+        @Provides
+        @Named(QS_USING_COLLAPSED_LANDSCAPE_MEDIA)
+        @JvmStatic
+        fun providesQSUsingCollapsedLandscapeMedia(context: Context): Boolean {
+            return false
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentComponent.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentComponent.java
index 594f4f8..327e858 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentComponent.java
@@ -16,53 +16,21 @@
 
 package com.android.systemui.qs.dagger;
 
-import com.android.systemui.qs.FooterActionsController;
-import com.android.systemui.qs.QSAnimator;
-import com.android.systemui.qs.QSContainerImplController;
-import com.android.systemui.qs.QSFooter;
-import com.android.systemui.qs.QSFragment;
-import com.android.systemui.qs.QSPanelController;
-import com.android.systemui.qs.QSSquishinessController;
-import com.android.systemui.qs.QuickQSPanelController;
-import com.android.systemui.qs.customize.QSCustomizerController;
+import com.android.systemui.qs.QSFragmentLegacy;
 
 import dagger.BindsInstance;
 import dagger.Subcomponent;
 
 /**
- * Dagger Subcomponent for {@link QSFragment}.
+ * Dagger Subcomponent for {@link QSFragmentLegacy}.
  */
 @Subcomponent(modules = {QSFragmentModule.class})
 @QSScope
-public interface QSFragmentComponent {
+public interface QSFragmentComponent extends QSComponent {
 
     /** Factory for building a {@link QSFragmentComponent}. */
     @Subcomponent.Factory
     interface Factory {
-        QSFragmentComponent create(@BindsInstance QSFragment qsFragment);
+        QSFragmentComponent create(@BindsInstance QSFragmentLegacy qsFragment);
     }
-
-    /** Construct a {@link QSPanelController}. */
-    QSPanelController getQSPanelController();
-
-    /** Construct a {@link QuickQSPanelController}. */
-    QuickQSPanelController getQuickQSPanelController();
-
-    /** Construct a {@link QSAnimator}. */
-    QSAnimator getQSAnimator();
-
-    /** Construct a {@link QSContainerImplController}. */
-    QSContainerImplController getQSContainerImplController();
-
-    /** Construct a {@link QSFooter} */
-    QSFooter getQSFooter();
-
-    /** Construct a {@link QSCustomizerController}. */
-    QSCustomizerController getQSCustomizerController();
-
-    /** Construct a {@link QSSquishinessController}. */
-    QSSquishinessController getQSSquishinessController();
-
-    /** Construct a {@link FooterActionsController}. */
-    FooterActionsController getQSFooterActionController();
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentModule.java
index bcd9803..0c9c24d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentModule.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentModule.java
@@ -20,21 +20,11 @@
 import static com.android.systemui.util.Utils.useQsMediaPlayer;
 
 import android.content.Context;
-import android.view.LayoutInflater;
 import android.view.View;
 
-import com.android.systemui.res.R;
 import com.android.systemui.dagger.qualifiers.RootView;
 import com.android.systemui.plugins.qs.QS;
-import com.android.systemui.qs.QSContainerImpl;
-import com.android.systemui.qs.QSFooter;
-import com.android.systemui.qs.QSFooterView;
-import com.android.systemui.qs.QSFooterViewController;
-import com.android.systemui.qs.QSFragment;
-import com.android.systemui.qs.QSPanel;
-import com.android.systemui.qs.QuickQSPanel;
-import com.android.systemui.qs.QuickStatusBarHeader;
-import com.android.systemui.qs.customize.QSCustomizer;
+import com.android.systemui.qs.QSFragmentLegacy;
 
 import javax.inject.Named;
 
@@ -45,93 +35,31 @@
 /**
  * Dagger Module for {@link QSFragmentComponent}.
  */
-@Module
-public interface QSFragmentModule {
-    String QS_USING_MEDIA_PLAYER = "qs_using_media_player";
-    String QS_USING_COLLAPSED_LANDSCAPE_MEDIA = "qs_using_collapsed_landscape_media";
+@Module(includes = {QSScopeModule.class})
+public  interface QSFragmentModule {
 
-    /**
-     * Provide a context themed using the QS theme
-     */
-    @Provides
-    @QSThemedContext
-    static Context provideThemedContext(@RootView View view) {
-        return view.getContext();
-    }
-
-    /** */
-    @Provides
-    @QSThemedContext
-    static LayoutInflater provideThemedLayoutInflater(@QSThemedContext Context context) {
-        return LayoutInflater.from(context);
-    }
-
-    /** */
     @Provides
     @RootView
-    static View provideRootView(QSFragment qsFragment) {
+    static View provideRootView(QSFragmentLegacy qsFragment) {
         return qsFragment.getView();
     }
 
     /** */
-    @Provides
-    static QSPanel provideQSPanel(@RootView View view) {
-        return view.findViewById(R.id.quick_settings_panel);
-    }
-
-    /** */
-    @Provides
-    static QSContainerImpl providesQSContainerImpl(@RootView View view) {
-        return view.findViewById(R.id.quick_settings_container);
-    }
-
-    /** */
     @Binds
-    QS bindQS(QSFragment qsFragment);
+    QS bindQS(QSFragmentLegacy qsFragment);
 
     /** */
     @Provides
-    static QuickStatusBarHeader providesQuickStatusBarHeader(@RootView View view) {
-        return view.findViewById(R.id.header);
-    }
-
-    /** */
-    @Provides
-    static QuickQSPanel providesQuickQSPanel(QuickStatusBarHeader quickStatusBarHeader) {
-        return quickStatusBarHeader.findViewById(R.id.quick_qs_panel);
-    }
-
-    /** */
-    @Provides
-    static QSFooterView providesQSFooterView(@RootView View view) {
-        return view.findViewById(R.id.qs_footer);
-    }
-
-    /** */
-    @Provides
-    @QSScope
-    static QSFooter providesQSFooter(QSFooterViewController qsFooterViewController) {
-        qsFooterViewController.init();
-        return qsFooterViewController;
-    }
-
-    /** */
-    @Provides
-    @QSScope
-    static QSCustomizer providesQSCutomizer(@RootView View view) {
-        return view.findViewById(R.id.qs_customize);
-    }
-
-    /** */
-    @Provides
-    @Named(QS_USING_MEDIA_PLAYER)
+    @Named(QSScopeModule.QS_USING_MEDIA_PLAYER)
     static boolean providesQSUsingMediaPlayer(Context context) {
         return useQsMediaPlayer(context);
     }
 
+
+
     /** */
     @Provides
-    @Named(QS_USING_COLLAPSED_LANDSCAPE_MEDIA)
+    @Named(QSScopeModule.QS_USING_COLLAPSED_LANDSCAPE_MEDIA)
     static boolean providesQSUsingCollapsedLandscapeMedia(Context context) {
         return useCollapsedMediaInLandscape(context.getResources());
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
index 03de3a0..92490e8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
@@ -52,7 +52,7 @@
 /**
  * Module for QS dependencies
  */
-@Module(subcomponents = {QSFragmentComponent.class},
+@Module(subcomponents = {QSFragmentComponent.class, QSFlexiglassComponent.class},
         includes = {
                 MediaModule.class,
                 QSExternalModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSScopeModule.kt b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSScopeModule.kt
new file mode 100644
index 0000000..e68ec4c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSScopeModule.kt
@@ -0,0 +1,92 @@
+package com.android.systemui.qs.dagger
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import com.android.systemui.dagger.qualifiers.RootView
+import com.android.systemui.qs.QSContainerImpl
+import com.android.systemui.qs.QSFooter
+import com.android.systemui.qs.QSFooterView
+import com.android.systemui.qs.QSFooterViewController
+import com.android.systemui.qs.QSPanel
+import com.android.systemui.qs.QuickQSPanel
+import com.android.systemui.qs.QuickStatusBarHeader
+import com.android.systemui.qs.customize.QSCustomizer
+import com.android.systemui.res.R
+import dagger.Module
+import dagger.Provides
+
+@Module
+interface QSScopeModule {
+    companion object {
+        const val QS_USING_MEDIA_PLAYER = "qs_using_media_player"
+        const val QS_USING_COLLAPSED_LANDSCAPE_MEDIA = "qs_using_collapsed_landscape_media"
+
+        @Provides
+        @QSThemedContext
+        @JvmStatic
+        fun provideThemedContext(@RootView view: View): Context {
+            return view.context
+        }
+
+        /**  */
+        @Provides
+        @QSThemedContext
+        @JvmStatic
+        fun provideThemedLayoutInflater(@QSThemedContext context: Context): LayoutInflater {
+            return LayoutInflater.from(context)
+        }
+
+        /**  */
+        @Provides
+        @JvmStatic
+        fun provideQSPanel(@RootView view: View): QSPanel {
+            return view.requireViewById<QSPanel>(R.id.quick_settings_panel)
+        }
+
+        /**  */
+        @Provides
+        @JvmStatic
+        fun providesQSContainerImpl(@RootView view: View): QSContainerImpl {
+            return view.requireViewById<QSContainerImpl>(R.id.quick_settings_container)
+        }
+
+        /**  */
+        @Provides
+        @JvmStatic
+        fun providesQuickStatusBarHeader(@RootView view: View): QuickStatusBarHeader {
+            return view.requireViewById<QuickStatusBarHeader>(R.id.header)
+        }
+
+        /**  */
+        @Provides
+        @JvmStatic
+        fun providesQuickQSPanel(quickStatusBarHeader: QuickStatusBarHeader): QuickQSPanel {
+            return quickStatusBarHeader.requireViewById<QuickQSPanel>(R.id.quick_qs_panel)
+        }
+
+        /**  */
+        @Provides
+        @JvmStatic
+        fun providesQSFooterView(@RootView view: View): QSFooterView {
+            return view.requireViewById<QSFooterView>(R.id.qs_footer)
+        }
+
+        /**  */
+        @Provides
+        @QSScope
+        @JvmStatic
+        fun providesQSFooter(qsFooterViewController: QSFooterViewController): QSFooter {
+            qsFooterViewController.init()
+            return qsFooterViewController
+        }
+
+        /**  */
+        @Provides
+        @QSScope
+        @JvmStatic
+        fun providesQSCutomizer(@RootView view: View): QSCustomizer {
+            return view.requireViewById<QSCustomizer>(R.id.qs_customize)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt
index b394a07..8b2c3de 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt
@@ -72,7 +72,7 @@
      * Show the device monitoring dialog, expanded from [expandable] if it's not null.
      *
      * Important: [quickSettingsContext] *must* be the [Context] associated to the
-     * [Quick Settings fragment][com.android.systemui.qs.QSFragment].
+     * [Quick Settings fragment][com.android.systemui.qs.QSFragmentLegacy].
      */
     fun showDeviceMonitoringDialog(quickSettingsContext: Context, expandable: Expandable?)
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
index 769cb1f..64fa33c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
@@ -23,7 +23,6 @@
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
 import com.android.settingslib.Utils
-import com.android.systemui.res.R
 import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
@@ -34,6 +33,7 @@
 import com.android.systemui.qs.dagger.QSFlagsModule.PM_LITE_ENABLED
 import com.android.systemui.qs.footer.data.model.UserSwitcherStatusModel
 import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractor
+import com.android.systemui.res.R
 import com.android.systemui.util.icuMessageFormat
 import javax.inject.Inject
 import javax.inject.Named
@@ -43,7 +43,6 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
@@ -201,8 +200,8 @@
      * will suspend indefinitely and will need to be cancelled to stop observing.
      *
      * Important: [quickSettingsContext] must be the [Context] associated to the
-     * [Quick Settings fragment][com.android.systemui.qs.QSFragment], and the call to this function
-     * must be cancelled when that fragment is destroyed.
+     * [Quick Settings fragment][com.android.systemui.qs.QSFragmentLegacy], and the call to this
+     * function must be cancelled when that fragment is destroyed.
      */
     suspend fun observeDeviceMonitoringDialogRequests(quickSettingsContext: Context) {
         footerActionsInteractor.deviceMonitoringDialogRequests.collect {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt
index a4600fb..21aaa94 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt
@@ -20,8 +20,12 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.LogBufferFactory
+import com.android.systemui.qs.pipeline.data.repository.DefaultTilesQSHostRepository
+import com.android.systemui.qs.pipeline.data.repository.DefaultTilesRepository
 import com.android.systemui.qs.pipeline.data.repository.InstalledTilesComponentRepository
 import com.android.systemui.qs.pipeline.data.repository.InstalledTilesComponentRepositoryImpl
+import com.android.systemui.qs.pipeline.data.repository.QSSettingsRestoredBroadcastRepository
+import com.android.systemui.qs.pipeline.data.repository.QSSettingsRestoredRepository
 import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository
 import com.android.systemui.qs.pipeline.data.repository.TileSpecSettingsRepository
 import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
@@ -42,6 +46,11 @@
     abstract fun provideTileSpecRepository(impl: TileSpecSettingsRepository): TileSpecRepository
 
     @Binds
+    abstract fun provideDefaultTilesRepository(
+        impl: DefaultTilesQSHostRepository
+    ): DefaultTilesRepository
+
+    @Binds
     abstract fun bindCurrentTilesInteractor(
         impl: CurrentTilesInteractorImpl
     ): CurrentTilesInteractor
@@ -56,6 +65,11 @@
     @ClassKey(QSPipelineCoreStartable::class)
     abstract fun provideCoreStartable(startable: QSPipelineCoreStartable): CoreStartable
 
+    @Binds
+    abstract fun provideQSSettingsRestoredRepository(
+        impl: QSSettingsRestoredBroadcastRepository
+    ): QSSettingsRestoredRepository
+
     companion object {
         /**
          * Provides a logging buffer for all logs related to the new Quick Settings pipeline to log
@@ -67,5 +81,12 @@
         fun provideQSTileListLogBuffer(factory: LogBufferFactory): LogBuffer {
             return factory.create(QSPipelineLogger.TILE_LIST_TAG, maxSize = 700, systrace = false)
         }
+
+        @Provides
+        @SysUISingleton
+        @QSRestoreLog
+        fun providesQSRestoreLogBuffer(factory: LogBufferFactory): LogBuffer {
+            return factory.create(QSPipelineLogger.RESTORE_TAG, maxSize = 50, systrace = false)
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSRestoreLog.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSRestoreLog.kt
new file mode 100644
index 0000000..c964929
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSRestoreLog.kt
@@ -0,0 +1,6 @@
+package com.android.systemui.qs.pipeline.dagger
+
+import javax.inject.Qualifier
+
+/** A [LogBuffer] for the QS pipeline to track restore of associated settings. */
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class QSRestoreLog
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/model/RestoreData.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/model/RestoreData.kt
new file mode 100644
index 0000000..d962632
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/model/RestoreData.kt
@@ -0,0 +1,10 @@
+package com.android.systemui.qs.pipeline.data.model
+
+import com.android.systemui.qs.pipeline.shared.TileSpec
+
+/** Data restored from Quick Settings as part of Backup & Restore. */
+data class RestoreData(
+    val restoredTiles: List<TileSpec>,
+    val restoredAutoAddedTiles: Set<TileSpec>,
+    val userId: Int,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/AutoAddRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/AutoAddRepository.kt
index 43a16b6..7998dfb 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/AutoAddRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/AutoAddRepository.kt
@@ -16,28 +16,19 @@
 
 package com.android.systemui.qs.pipeline.data.repository
 
-import android.database.ContentObserver
-import android.provider.Settings
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import android.util.SparseArray
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.pipeline.data.model.RestoreData
 import com.android.systemui.qs.pipeline.shared.TileSpec
-import com.android.systemui.util.settings.SecureSettings
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.withContext
+import javax.inject.Inject
+import kotlinx.coroutines.flow.StateFlow
 
 /** Repository to track what QS tiles have been auto-added */
 interface AutoAddRepository {
 
     /** Flow of tiles that have been auto-added */
-    fun autoAddedTiles(userId: Int): Flow<Set<TileSpec>>
+    suspend fun autoAddedTiles(userId: Int): Flow<Set<TileSpec>>
 
     /** Mark a tile as having been auto-added */
     suspend fun markTileAdded(userId: Int, spec: TileSpec)
@@ -47,89 +38,39 @@
      * multiple times.
      */
     suspend fun unmarkTileAdded(userId: Int, spec: TileSpec)
+
+    suspend fun reconcileRestore(restoreData: RestoreData)
 }
 
 /**
- * Implementation that tracks the auto-added tiles stored in [Settings.Secure.QS_AUTO_ADDED_TILES].
+ * Implementation of [AutoAddRepository] that delegates to an instance of [UserAutoAddRepository]
+ * for each user.
  */
 @SysUISingleton
 class AutoAddSettingRepository
 @Inject
-constructor(
-    private val secureSettings: SecureSettings,
-    @Background private val bgDispatcher: CoroutineDispatcher,
-) : AutoAddRepository {
-    override fun autoAddedTiles(userId: Int): Flow<Set<TileSpec>> {
-        return conflatedCallbackFlow {
-                val observer =
-                    object : ContentObserver(null) {
-                        override fun onChange(selfChange: Boolean) {
-                            trySend(Unit)
-                        }
-                    }
+constructor(private val userAutoAddRepositoryFactory: UserAutoAddRepository.Factory) :
+    AutoAddRepository {
 
-                secureSettings.registerContentObserverForUser(SETTING, observer, userId)
+    private val userAutoAddRepositories = SparseArray<UserAutoAddRepository>()
 
-                awaitClose { secureSettings.unregisterContentObserver(observer) }
-            }
-            .onStart { emit(Unit) }
-            .map { secureSettings.getStringForUser(SETTING, userId) ?: "" }
-            .distinctUntilChanged()
-            .map {
-                it.split(DELIMITER).map(TileSpec::create).filter { it !is TileSpec.Invalid }.toSet()
-            }
-            .flowOn(bgDispatcher)
+    override suspend fun autoAddedTiles(userId: Int): Flow<Set<TileSpec>> {
+        if (userId !in userAutoAddRepositories) {
+            val repository = userAutoAddRepositoryFactory.create(userId)
+            userAutoAddRepositories.put(userId, repository)
+        }
+        return userAutoAddRepositories.get(userId).autoAdded()
     }
 
     override suspend fun markTileAdded(userId: Int, spec: TileSpec) {
-        if (spec is TileSpec.Invalid) {
-            return
-        }
-        val added = load(userId).toMutableSet()
-        if (added.add(spec)) {
-            store(userId, added)
-        }
+        userAutoAddRepositories.get(userId)?.markTileAdded(spec)
     }
 
     override suspend fun unmarkTileAdded(userId: Int, spec: TileSpec) {
-        if (spec is TileSpec.Invalid) {
-            return
-        }
-        val added = load(userId).toMutableSet()
-        if (added.remove(spec)) {
-            store(userId, added)
-        }
+        userAutoAddRepositories.get(userId)?.unmarkTileAdded(spec)
     }
 
-    private suspend fun store(userId: Int, tiles: Set<TileSpec>) {
-        val toStore =
-            tiles
-                .filter { it !is TileSpec.Invalid }
-                .joinToString(DELIMITER, transform = TileSpec::spec)
-        withContext(bgDispatcher) {
-            secureSettings.putStringForUser(
-                SETTING,
-                toStore,
-                null,
-                false,
-                userId,
-                true,
-            )
-        }
-    }
-
-    private suspend fun load(userId: Int): Set<TileSpec> {
-        return withContext(bgDispatcher) {
-            (secureSettings.getStringForUser(SETTING, userId) ?: "")
-                .split(",")
-                .map(TileSpec::create)
-                .filter { it !is TileSpec.Invalid }
-                .toSet()
-        }
-    }
-
-    companion object {
-        private const val SETTING = Settings.Secure.QS_AUTO_ADDED_TILES
-        private const val DELIMITER = ","
+    override suspend fun reconcileRestore(restoreData: RestoreData) {
+        userAutoAddRepositories.get(restoreData.userId)?.reconcileRestore(restoreData)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/DefaultTilesRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/DefaultTilesRepository.kt
new file mode 100644
index 0000000..fe0a69b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/DefaultTilesRepository.kt
@@ -0,0 +1,25 @@
+package com.android.systemui.qs.pipeline.data.repository
+
+import android.content.res.Resources
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.QSHost
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import javax.inject.Inject
+
+interface DefaultTilesRepository {
+    val defaultTiles: List<TileSpec>
+}
+
+@SysUISingleton
+class DefaultTilesQSHostRepository
+@Inject
+constructor(
+    @Main private val resources: Resources,
+) : DefaultTilesRepository {
+    override val defaultTiles: List<TileSpec>
+        get() =
+            QSHost.getDefaultSpecs(resources).map(TileSpec::create).filter {
+                it != TileSpec.Invalid
+            }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/QSSettingsRestoredRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/QSSettingsRestoredRepository.kt
new file mode 100644
index 0000000..6cee116
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/QSSettingsRestoredRepository.kt
@@ -0,0 +1,122 @@
+package com.android.systemui.qs.pipeline.data.repository
+
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.UserHandle
+import android.provider.Settings
+import android.util.Log
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.pipeline.data.model.RestoreData
+import com.android.systemui.qs.pipeline.data.repository.TilesSettingConverter.toTilesList
+import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.buffer
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.shareIn
+
+/** Provides restored data (from Backup and Restore) for Quick Settings pipeline */
+interface QSSettingsRestoredRepository {
+    val restoreData: Flow<RestoreData>
+}
+
+@SysUISingleton
+class QSSettingsRestoredBroadcastRepository
+@Inject
+constructor(
+    broadcastDispatcher: BroadcastDispatcher,
+    logger: QSPipelineLogger,
+    @Application private val scope: CoroutineScope,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+) : QSSettingsRestoredRepository {
+
+    override val restoreData =
+        flow {
+                val firstIntent = mutableMapOf<Int, Intent>()
+                broadcastDispatcher
+                    .broadcastFlow(INTENT_FILTER, UserHandle.ALL) { intent, receiver ->
+                        intent to receiver.sendingUserId
+                    }
+                    .filter { it.first.isCorrectSetting() }
+                    .collect { (intent, user) ->
+                        if (user !in firstIntent) {
+                            firstIntent[user] = intent
+                        } else {
+                            val firstRestored = firstIntent.remove(user)!!
+                            emit(processIntents(user, firstRestored, intent))
+                        }
+                    }
+            }
+            .catch { Log.e(TAG, "Error parsing broadcast", it) }
+            .flowOn(backgroundDispatcher)
+            .buffer(BUFFER_CAPACITY)
+            .shareIn(scope, SharingStarted.Eagerly)
+            .onEach(logger::logSettingsRestored)
+
+    private fun processIntents(user: Int, intent1: Intent, intent2: Intent): RestoreData {
+        intent1.validateIntent()
+        intent2.validateIntent()
+        val setting1 = intent1.getStringExtra(Intent.EXTRA_SETTING_NAME)
+        val setting2 = intent2.getStringExtra(Intent.EXTRA_SETTING_NAME)
+        val (tiles, autoAdd) =
+            if (setting1 == TILES_SETTING && setting2 == AUTO_ADD_SETTING) {
+                intent1 to intent2
+            } else if (setting1 == AUTO_ADD_SETTING && setting2 == TILES_SETTING) {
+                intent2 to intent1
+            } else {
+                throw IllegalStateException("Wrong intents ($intent1, $intent2)")
+            }
+
+        return RestoreData(
+            (tiles.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE) ?: "").toTilesList(),
+            (autoAdd.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE) ?: "").toTilesSet(),
+            user,
+        )
+    }
+
+    private companion object {
+        private const val TAG = "QSSettingsRestoredBroadcastRepository"
+        // This capacity is the number of restore data that we will keep buffered in the shared
+        // flow. It is unlikely that at any given time there would be this many restores being
+        // processed by consumers, but just in case that a couple of users are restored at the
+        // same time and they need to be replayed for the consumers of the flow.
+        private const val BUFFER_CAPACITY = 10
+
+        private val INTENT_FILTER = IntentFilter(Intent.ACTION_SETTING_RESTORED)
+        private const val TILES_SETTING = Settings.Secure.QS_TILES
+        private const val AUTO_ADD_SETTING = Settings.Secure.QS_AUTO_ADDED_TILES
+        private val requiredExtras =
+            listOf(
+                Intent.EXTRA_SETTING_NAME,
+                Intent.EXTRA_SETTING_PREVIOUS_VALUE,
+                Intent.EXTRA_SETTING_NEW_VALUE,
+            )
+
+        private fun Intent.isCorrectSetting(): Boolean {
+            val setting = getStringExtra(Intent.EXTRA_SETTING_NAME)
+            return setting == TILES_SETTING || setting == AUTO_ADD_SETTING
+        }
+
+        private fun Intent.validateIntent() {
+            requiredExtras.forEach { extra ->
+                if (!hasExtra(extra)) {
+                    throw IllegalStateException("$this doesn't have $extra")
+                }
+            }
+        }
+
+        private fun String.toTilesList() = toTilesList(this)
+
+        private fun String.toTilesSet() = TilesSettingConverter.toTilesSet(this)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt
index 47c99f2..00ea0b5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt
@@ -18,34 +18,19 @@
 
 import android.annotation.UserIdInt
 import android.content.res.Resources
-import android.database.ContentObserver
-import android.provider.Settings
 import android.util.SparseArray
 import com.android.systemui.res.R
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.qs.QSHost
+import com.android.systemui.qs.pipeline.data.model.RestoreData
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
 import com.android.systemui.retail.data.repository.RetailModeRepository
-import com.android.systemui.util.settings.SecureSettings
 import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-import kotlinx.coroutines.withContext
 
 /** Repository that tracks the current tiles. */
 interface TileSpecRepository {
@@ -55,7 +40,7 @@
      *
      * Tiles will never be [TileSpec.Invalid] in the list and it will never be empty.
      */
-    fun tilesSpecs(@UserIdInt userId: Int): Flow<List<TileSpec>>
+    suspend fun tilesSpecs(@UserIdInt userId: Int): Flow<List<TileSpec>>
 
     /**
      * Adds a [tile] for a given [userId] at [position]. Using [POSITION_AT_END] will add the tile
@@ -81,6 +66,8 @@
      */
     suspend fun setTiles(@UserIdInt userId: Int, tiles: List<TileSpec>)
 
+    suspend fun reconcileRestore(restoreData: RestoreData, currentAutoAdded: Set<TileSpec>)
+
     companion object {
         /** Position to indicate the end of the list */
         const val POSITION_AT_END = -1
@@ -88,28 +75,23 @@
 }
 
 /**
- * Implementation of [TileSpecRepository] that persist the values of tiles in
- * [Settings.Secure.QS_TILES].
- *
- * All operations against [Settings] will be performed in a background thread.
+ * Implementation of [TileSpecRepository] that delegates to an instance of [UserTileSpecRepository]
+ * for each user.
  *
  * If the device is in retail mode, the tiles are fixed to the value of
  * [R.string.quick_settings_tiles_retail_mode].
  */
+@OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class TileSpecSettingsRepository
 @Inject
 constructor(
-    private val secureSettings: SecureSettings,
     @Main private val resources: Resources,
     private val logger: QSPipelineLogger,
     private val retailModeRepository: RetailModeRepository,
-    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val userTileSpecRepositoryFactory: UserTileSpecRepository.Factory,
 ) : TileSpecRepository {
 
-    private val mutex = Mutex()
-    private val tileSpecsPerUser = SparseArray<List<TileSpec>>()
-
     private val retailModeTiles by lazy {
         resources
             .getString(R.string.quick_settings_tiles_retail_mode)
@@ -118,123 +100,59 @@
             .filter { it !is TileSpec.Invalid }
     }
 
-    @OptIn(ExperimentalCoroutinesApi::class)
-    override fun tilesSpecs(userId: Int): Flow<List<TileSpec>> {
+    private val userTileRepositories = SparseArray<UserTileSpecRepository>()
+
+    override suspend fun tilesSpecs(userId: Int): Flow<List<TileSpec>> {
+        if (userId !in userTileRepositories) {
+            val userTileRepository = userTileSpecRepositoryFactory.create(userId)
+            userTileRepositories.put(userId, userTileRepository)
+        }
+        val realTiles = userTileRepositories.get(userId).tiles()
+
         return retailModeRepository.retailMode.flatMapLatest { inRetailMode ->
             if (inRetailMode) {
                 logger.logUsingRetailTiles()
                 flowOf(retailModeTiles)
             } else {
-                settingsTiles(userId)
+                realTiles
             }
         }
     }
 
-    private fun settingsTiles(userId: Int): Flow<List<TileSpec>> {
-        return conflatedCallbackFlow {
-                val observer =
-                    object : ContentObserver(null) {
-                        override fun onChange(selfChange: Boolean) {
-                            trySend(Unit)
-                        }
-                    }
-
-                secureSettings.registerContentObserverForUser(SETTING, observer, userId)
-
-                awaitClose { secureSettings.unregisterContentObserver(observer) }
-            }
-            .onStart { emit(Unit) }
-            .map { loadTiles(userId) }
-            .onEach { logger.logTilesChangedInSettings(it, userId) }
-            .distinctUntilChanged()
-            .map { parseTileSpecs(it, userId).also { storeTiles(userId, it) } }
-            .distinctUntilChanged()
-            .onEach { mutex.withLock { tileSpecsPerUser.put(userId, it) } }
-            .flowOn(backgroundDispatcher)
-    }
-
-    override suspend fun addTile(userId: Int, tile: TileSpec, position: Int) =
-        mutex.withLock {
-            if (tile == TileSpec.Invalid) {
-                return
-            }
-            val tilesList = tileSpecsPerUser.get(userId, emptyList()).toMutableList()
-            if (tile !in tilesList) {
-                if (position < 0 || position >= tilesList.size) {
-                    tilesList.add(tile)
-                } else {
-                    tilesList.add(position, tile)
-                }
-                storeTiles(userId, tilesList)
-                tileSpecsPerUser.put(userId, tilesList)
-            }
-        }
-
-    override suspend fun removeTiles(userId: Int, tiles: Collection<TileSpec>) =
-        mutex.withLock {
-            if (tiles.all { it == TileSpec.Invalid }) {
-                return
-            }
-            val tilesList = tileSpecsPerUser.get(userId, emptyList()).toMutableList()
-            if (tilesList.removeAll(tiles)) {
-                storeTiles(userId, tilesList.toList())
-                tileSpecsPerUser.put(userId, tilesList)
-            }
-        }
-
-    override suspend fun setTiles(userId: Int, tiles: List<TileSpec>) =
-        mutex.withLock {
-            val filtered = tiles.filter { it != TileSpec.Invalid }
-            if (filtered.isNotEmpty()) {
-                storeTiles(userId, filtered)
-                tileSpecsPerUser.put(userId, tiles)
-            }
-        }
-
-    private suspend fun storeTiles(@UserIdInt forUser: Int, tiles: List<TileSpec>) {
+    override suspend fun addTile(userId: Int, tile: TileSpec, position: Int) {
         if (retailModeRepository.inRetailMode) {
-            // No storing tiles when in retail mode
             return
         }
-        val toStore =
-            tiles
-                .filter { it !is TileSpec.Invalid }
-                .joinToString(DELIMITER, transform = TileSpec::spec)
-        withContext(backgroundDispatcher) {
-            secureSettings.putStringForUser(
-                SETTING,
-                toStore,
-                null,
-                false,
-                forUser,
-                true,
-            )
+        if (tile is TileSpec.Invalid) {
+            return
         }
+        userTileRepositories.get(userId)?.addTile(tile, position)
     }
 
-    private suspend fun loadTiles(userId: Int): String {
-        return withContext(backgroundDispatcher) {
-            secureSettings.getStringForUser(SETTING, userId) ?: ""
+    override suspend fun removeTiles(userId: Int, tiles: Collection<TileSpec>) {
+        if (retailModeRepository.inRetailMode) {
+            return
         }
+        userTileRepositories.get(userId)?.removeTiles(tiles)
     }
 
-    private fun parseTileSpecs(tilesFromSettings: String, user: Int): List<TileSpec> {
-        val fromSettings =
-            tilesFromSettings.split(DELIMITER).map(TileSpec::create).filter {
-                it != TileSpec.Invalid
-            }
-        return if (fromSettings.isNotEmpty()) {
-            fromSettings.also { logger.logParsedTiles(it, false, user) }
-        } else {
-            QSHost.getDefaultSpecs(resources)
-                .map(TileSpec::create)
-                .filter { it != TileSpec.Invalid }
-                .also { logger.logParsedTiles(it, true, user) }
+    override suspend fun setTiles(userId: Int, tiles: List<TileSpec>) {
+        if (retailModeRepository.inRetailMode) {
+            return
         }
+        userTileRepositories.get(userId)?.setTiles(tiles)
+    }
+
+    override suspend fun reconcileRestore(
+        restoreData: RestoreData,
+        currentAutoAdded: Set<TileSpec>
+    ) {
+        userTileRepositories
+            .get(restoreData.userId)
+            ?.reconcileRestore(restoreData, currentAutoAdded)
     }
 
     companion object {
-        private const val SETTING = Settings.Secure.QS_TILES
-        private const val DELIMITER = ","
+        private const val DELIMITER = TilesSettingConverter.DELIMITER
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TilesSettingConverter.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TilesSettingConverter.kt
new file mode 100644
index 0000000..bb55fcd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TilesSettingConverter.kt
@@ -0,0 +1,18 @@
+package com.android.systemui.qs.pipeline.data.repository
+
+import com.android.systemui.qs.pipeline.shared.TileSpec
+
+object TilesSettingConverter {
+
+    const val DELIMITER = ","
+
+    fun toTilesList(commaSeparatedTiles: String) =
+        commaSeparatedTiles.split(DELIMITER).map(TileSpec::create).filter { it != TileSpec.Invalid }
+
+    fun toTilesSet(commaSeparatedTiles: String) =
+        commaSeparatedTiles
+            .split(DELIMITER)
+            .map(TileSpec::create)
+            .filter { it != TileSpec.Invalid }
+            .toSet()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserAutoAddRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserAutoAddRepository.kt
new file mode 100644
index 0000000..d452241
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserAutoAddRepository.kt
@@ -0,0 +1,186 @@
+package com.android.systemui.qs.pipeline.data.repository
+
+import android.database.ContentObserver
+import android.provider.Settings
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.pipeline.data.model.RestoreData
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
+import com.android.systemui.util.settings.SecureSettings
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.scan
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/**
+ * Single user version of [AutoAddRepository]. It provides a similar interface as
+ * [AutoAddRepository], but focusing solely on the user it was created for.
+ *
+ * This is the source of truth for that user's tiles, after the user has been started. Persisting
+ * all the changes to [Settings]. Changes in [Settings] that disagree with this repository will be
+ * reverted
+ *
+ * All operations against [Settings] will be performed in a background thread.
+ */
+class UserAutoAddRepository
+@AssistedInject
+constructor(
+    @Assisted private val userId: Int,
+    private val secureSettings: SecureSettings,
+    private val logger: QSPipelineLogger,
+    @Application private val applicationScope: CoroutineScope,
+    @Background private val bgDispatcher: CoroutineDispatcher,
+) {
+
+    private val changeEvents = MutableSharedFlow<ChangeAction>(
+        extraBufferCapacity = CHANGES_BUFFER_SIZE
+    )
+
+    private lateinit var _autoAdded: StateFlow<Set<TileSpec>>
+
+    suspend fun autoAdded(): StateFlow<Set<TileSpec>> {
+        if (!::_autoAdded.isInitialized) {
+            _autoAdded =
+                changeEvents
+                    .scan(load().also { logger.logAutoAddTilesParsed(userId, it) }) {
+                        current,
+                        change ->
+                        change.apply(current).also {
+                            if (change is RestoreTiles) {
+                                logger.logAutoAddTilesRestoredReconciled(userId, it)
+                            }
+                        }
+                    }
+                    .flowOn(bgDispatcher)
+                    .stateIn(applicationScope)
+                    .also { startFlowCollections(it) }
+        }
+        return _autoAdded
+    }
+
+    private fun startFlowCollections(autoAdded: StateFlow<Set<TileSpec>>) {
+        applicationScope.launch(bgDispatcher) {
+            launch { autoAdded.collect { store(it) } }
+            launch {
+                // As Settings is not the source of truth, once we started tracking tiles for a
+                // user, we don't want anyone to change the underlying setting. Therefore, if there
+                // are any changes that don't match with the source of truth (this class), we
+                // overwrite them with the current value.
+                ConflatedCallbackFlow.conflatedCallbackFlow {
+                        val observer =
+                            object : ContentObserver(null) {
+                                override fun onChange(selfChange: Boolean) {
+                                    trySend(Unit)
+                                }
+                            }
+                        secureSettings.registerContentObserverForUser(SETTING, observer, userId)
+                        awaitClose { secureSettings.unregisterContentObserver(observer) }
+                    }
+                    .map { load() }
+                    .flowOn(bgDispatcher)
+                    .collect { setting ->
+                        val current = autoAdded.value
+                        if (setting != current) {
+                            store(current)
+                        }
+                    }
+            }
+        }
+    }
+
+    suspend fun markTileAdded(spec: TileSpec) {
+        if (spec is TileSpec.Invalid) {
+            return
+        }
+        changeEvents.emit(MarkTile(spec))
+    }
+
+    suspend fun unmarkTileAdded(spec: TileSpec) {
+        if (spec is TileSpec.Invalid) {
+            return
+        }
+        changeEvents.emit(UnmarkTile(spec))
+    }
+
+    private suspend fun store(tiles: Set<TileSpec>) {
+        val toStore =
+            tiles
+                .filter { it !is TileSpec.Invalid }
+                .joinToString(DELIMITER, transform = TileSpec::spec)
+        withContext(bgDispatcher) {
+            secureSettings.putStringForUser(
+                SETTING,
+                toStore,
+                null,
+                false,
+                userId,
+                true,
+            )
+        }
+    }
+
+    private suspend fun load(): Set<TileSpec> {
+        return withContext(bgDispatcher) {
+            (secureSettings.getStringForUser(SETTING, userId) ?: "").toTilesSet()
+        }
+    }
+
+    suspend fun reconcileRestore(restoreData: RestoreData) {
+        changeEvents.emit(RestoreTiles(restoreData))
+    }
+
+    private sealed interface ChangeAction {
+        fun apply(currentAutoAdded: Set<TileSpec>): Set<TileSpec>
+    }
+
+    private data class MarkTile(
+        val tileSpec: TileSpec,
+    ) : ChangeAction {
+        override fun apply(currentAutoAdded: Set<TileSpec>): Set<TileSpec> {
+            return currentAutoAdded.toMutableSet().apply { add(tileSpec) }
+        }
+    }
+
+    private data class UnmarkTile(
+        val tileSpec: TileSpec,
+    ) : ChangeAction {
+        override fun apply(currentAutoAdded: Set<TileSpec>): Set<TileSpec> {
+            return currentAutoAdded.toMutableSet().apply { remove(tileSpec) }
+        }
+    }
+
+    private data class RestoreTiles(
+        val restoredData: RestoreData,
+    ) : ChangeAction {
+        override fun apply(currentAutoAdded: Set<TileSpec>): Set<TileSpec> {
+            return currentAutoAdded + restoredData.restoredAutoAddedTiles
+        }
+    }
+
+    companion object {
+        private const val SETTING = Settings.Secure.QS_AUTO_ADDED_TILES
+        private const val DELIMITER = ","
+        // We want a small buffer in case multiple changes come in at the same time (sometimes
+        // happens in first start. This should be enough to not lose changes.
+        private const val CHANGES_BUFFER_SIZE = 10
+
+        private fun String.toTilesSet() = TilesSettingConverter.toTilesSet(this)
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(userId: Int): UserAutoAddRepository
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt
new file mode 100644
index 0000000..152fd0f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt
@@ -0,0 +1,252 @@
+package com.android.systemui.qs.pipeline.data.repository
+
+import android.annotation.UserIdInt
+import android.database.ContentObserver
+import android.provider.Settings
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.pipeline.data.model.RestoreData
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
+import com.android.systemui.util.settings.SecureSettings
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.scan
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/**
+ * Single user version of [TileSpecRepository]. It provides a similar interface as
+ * [TileSpecRepository], but focusing solely on the user it was created for.
+ *
+ * This is the source of truth for that user's tiles, after the user has been started. Persisting
+ * all the changes to [Settings]. Changes in [Settings] that disagree with this repository will be
+ * reverted
+ *
+ * All operations against [Settings] will be performed in a background thread.
+ */
+class UserTileSpecRepository
+@AssistedInject
+constructor(
+    @Assisted private val userId: Int,
+    private val defaultTilesRepository: DefaultTilesRepository,
+    private val secureSettings: SecureSettings,
+    private val logger: QSPipelineLogger,
+    @Application private val applicationScope: CoroutineScope,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+) {
+
+    private val defaultTiles: List<TileSpec>
+        get() = defaultTilesRepository.defaultTiles
+
+    private val changeEvents = MutableSharedFlow<ChangeAction>(
+        extraBufferCapacity = CHANGES_BUFFER_SIZE
+    )
+
+    private lateinit var _tiles: StateFlow<List<TileSpec>>
+
+    suspend fun tiles(): Flow<List<TileSpec>> {
+        if (!::_tiles.isInitialized) {
+            _tiles =
+                changeEvents
+                    .scan(loadTilesFromSettingsAndParse(userId)) { current, change ->
+                        change.apply(current).also {
+                            if (current != it) {
+                                if (change is RestoreTiles) {
+                                    logger.logTilesRestoredAndReconciled(current, it, userId)
+                                } else {
+                                    logger.logProcessTileChange(change, it, userId)
+                                }
+                            }
+                        }
+                    }
+                    .flowOn(backgroundDispatcher)
+                    .stateIn(applicationScope)
+                    .also { startFlowCollections(it) }
+        }
+        return _tiles
+    }
+
+    private fun startFlowCollections(tiles: StateFlow<List<TileSpec>>) {
+        applicationScope.launch(backgroundDispatcher) {
+            launch { tiles.collect { storeTiles(userId, it) } }
+            launch {
+                // As Settings is not the source of truth, once we started tracking tiles for a
+                // user, we don't want anyone to change the underlying setting. Therefore, if there
+                // are any changes that don't match with the source of truth (this class), we
+                // overwrite them with the current value.
+                ConflatedCallbackFlow.conflatedCallbackFlow {
+                        val observer =
+                            object : ContentObserver(null) {
+                                override fun onChange(selfChange: Boolean) {
+                                    trySend(Unit)
+                                }
+                            }
+                        secureSettings.registerContentObserverForUser(SETTING, observer, userId)
+                        awaitClose { secureSettings.unregisterContentObserver(observer) }
+                    }
+                    .map { loadTilesFromSettings(userId) }
+                    .flowOn(backgroundDispatcher)
+                    .collect { setting ->
+                        val current = tiles.value
+                        if (setting != current) {
+                            storeTiles(userId, current)
+                        }
+                    }
+            }
+        }
+    }
+
+    private suspend fun storeTiles(@UserIdInt forUser: Int, tiles: List<TileSpec>) {
+        val toStore =
+            tiles
+                .filter { it !is TileSpec.Invalid }
+                .joinToString(DELIMITER, transform = TileSpec::spec)
+        withContext(backgroundDispatcher) {
+            secureSettings.putStringForUser(
+                SETTING,
+                toStore,
+                null,
+                false,
+                forUser,
+                true,
+            )
+        }
+    }
+
+    suspend fun addTile(tile: TileSpec, position: Int = TileSpecRepository.POSITION_AT_END) {
+        if (tile is TileSpec.Invalid) {
+            return
+        }
+        changeEvents.emit(AddTile(tile, position))
+    }
+
+    suspend fun removeTiles(tiles: Collection<TileSpec>) {
+        changeEvents.emit(RemoveTiles(tiles))
+    }
+
+    suspend fun setTiles(tiles: List<TileSpec>) {
+        changeEvents.emit(ChangeTiles(tiles))
+    }
+
+    private fun parseTileSpecs(fromSettings: List<TileSpec>, user: Int): List<TileSpec> {
+        return if (fromSettings.isNotEmpty()) {
+            fromSettings.also { logger.logParsedTiles(it, false, user) }
+        } else {
+            defaultTiles.also { logger.logParsedTiles(it, true, user) }
+        }
+    }
+
+    private suspend fun loadTilesFromSettingsAndParse(userId: Int): List<TileSpec> {
+        return parseTileSpecs(loadTilesFromSettings(userId), userId)
+    }
+
+    private suspend fun loadTilesFromSettings(userId: Int): List<TileSpec> {
+        return withContext(backgroundDispatcher) {
+                secureSettings.getStringForUser(SETTING, userId) ?: ""
+            }
+            .toTilesList()
+    }
+
+    suspend fun reconcileRestore(restoreData: RestoreData, currentAutoAdded: Set<TileSpec>) {
+        changeEvents.emit(RestoreTiles(restoreData, currentAutoAdded))
+    }
+
+    sealed interface ChangeAction {
+        fun apply(currentTiles: List<TileSpec>): List<TileSpec>
+    }
+
+    private data class AddTile(
+        val tileSpec: TileSpec,
+        val position: Int = TileSpecRepository.POSITION_AT_END
+    ) : ChangeAction {
+        override fun apply(currentTiles: List<TileSpec>): List<TileSpec> {
+            val tilesList = currentTiles.toMutableList()
+            if (tileSpec !in tilesList) {
+                if (position < 0 || position >= tilesList.size) {
+                    tilesList.add(tileSpec)
+                } else {
+                    tilesList.add(position, tileSpec)
+                }
+            }
+            return tilesList
+        }
+    }
+
+    private data class RemoveTiles(val tileSpecs: Collection<TileSpec>) : ChangeAction {
+        override fun apply(currentTiles: List<TileSpec>): List<TileSpec> {
+            return currentTiles.toMutableList().apply { removeAll(tileSpecs) }
+        }
+    }
+
+    private data class ChangeTiles(
+        val newTiles: List<TileSpec>,
+    ) : ChangeAction {
+        override fun apply(currentTiles: List<TileSpec>): List<TileSpec> {
+            val new = newTiles.filter { it !is TileSpec.Invalid }
+            return if (new.isNotEmpty()) new else currentTiles
+        }
+    }
+
+    private data class RestoreTiles(
+        val restoreData: RestoreData,
+        val currentAutoAdded: Set<TileSpec>,
+    ) : ChangeAction {
+
+        override fun apply(currentTiles: List<TileSpec>): List<TileSpec> {
+            return reconcileTiles(currentTiles, currentAutoAdded, restoreData)
+        }
+    }
+
+    companion object {
+        private const val SETTING = Settings.Secure.QS_TILES
+        private const val DELIMITER = TilesSettingConverter.DELIMITER
+        // We want a small buffer in case multiple changes come in at the same time (sometimes
+        // happens in first start. This should be enough to not lose changes.
+        private const val CHANGES_BUFFER_SIZE = 10
+
+        private fun String.toTilesList() = TilesSettingConverter.toTilesList(this)
+
+        fun reconcileTiles(
+            currentTiles: List<TileSpec>,
+            currentAutoAdded: Set<TileSpec>,
+            restoreData: RestoreData
+        ): List<TileSpec> {
+            val toRestore = restoreData.restoredTiles.toMutableList()
+            val freshlyAutoAdded =
+                currentAutoAdded.filterNot { it in restoreData.restoredAutoAddedTiles }
+            freshlyAutoAdded
+                .filter { it in currentTiles && it !in restoreData.restoredTiles }
+                .map { it to currentTiles.indexOf(it) }
+                .sortedBy { it.second }
+                .forEachIndexed { iteration, (tile, position) ->
+                    val insertAt = position + iteration
+                    if (insertAt > toRestore.size) {
+                        toRestore.add(tile)
+                    } else {
+                        toRestore.add(insertAt, tile)
+                    }
+                }
+
+            return toRestore
+        }
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(
+            userId: Int,
+        ): UserTileSpecRepository
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
index 5a5e47a..00c2358 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
@@ -20,6 +20,7 @@
 import android.content.Context
 import android.content.Intent
 import android.os.UserHandle
+import android.util.Log
 import com.android.systemui.Dumpable
 import com.android.systemui.ProtoDumpable
 import com.android.systemui.dagger.SysUISingleton
@@ -268,6 +269,7 @@
                             // repository
                             launch { tileSpecRepository.setTiles(currentUser.value, resolvedSpecs) }
                         }
+                        Log.d("Fabian", "Finished resolving tiles")
                     }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/RestoreReconciliationInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/RestoreReconciliationInteractor.kt
new file mode 100644
index 0000000..9844903
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/RestoreReconciliationInteractor.kt
@@ -0,0 +1,53 @@
+package com.android.systemui.qs.pipeline.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.pipeline.data.repository.AutoAddRepository
+import com.android.systemui.qs.pipeline.data.repository.QSSettingsRestoredRepository
+import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.flatMapConcat
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.launch
+
+/**
+ * Interactor in charge of triggering reconciliation after QS Secure Settings are restored. For a
+ * given user, it will trigger the reconciliations in the correct order to prevent race conditions.
+ *
+ * Currently, the order is:
+ * 1. TileSpecRepository, with the restored data and the current (before restore) auto add tiles
+ * 2. AutoAddRepository
+ *
+ * [start] needs to be called to trigger the collection of [QSSettingsRestoredRepository].
+ */
+@SysUISingleton
+class RestoreReconciliationInteractor
+@Inject
+constructor(
+    private val tileSpecRepository: TileSpecRepository,
+    private val autoAddRepository: AutoAddRepository,
+    private val qsSettingsRestoredRepository: QSSettingsRestoredRepository,
+    @Application private val applicationScope: CoroutineScope,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+) {
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    fun start() {
+        applicationScope.launch(backgroundDispatcher) {
+            qsSettingsRestoredRepository.restoreData.flatMapConcat { data ->
+                autoAddRepository.autoAddedTiles(data.userId)
+                        .take(1)
+                        .map { tiles -> data to tiles }
+            }.collect { (restoreData, autoAdded) ->
+                tileSpecRepository.reconcileRestore(restoreData, autoAdded)
+                autoAddRepository.reconcileRestore(restoreData)
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt
index 0743ba0..1539f05 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/startable/QSPipelineCoreStartable.kt
@@ -20,6 +20,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.qs.pipeline.domain.interactor.AutoAddInteractor
 import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
+import com.android.systemui.qs.pipeline.domain.interactor.RestoreReconciliationInteractor
 import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository
 import javax.inject.Inject
 
@@ -30,11 +31,13 @@
     private val currentTilesInteractor: CurrentTilesInteractor,
     private val autoAddInteractor: AutoAddInteractor,
     private val featureFlags: QSPipelineFlagsRepository,
+    private val restoreReconciliationInteractor: RestoreReconciliationInteractor,
 ) : CoreStartable {
 
     override fun start() {
         if (featureFlags.pipelineAutoAddEnabled) {
             autoAddInteractor.init(currentTilesInteractor)
+            restoreReconciliationInteractor.start()
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt
index 573cb715..bca86c9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt
@@ -16,11 +16,13 @@
 
 package com.android.systemui.qs.pipeline.shared.logging
 
-import android.annotation.UserIdInt
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.LogLevel
 import com.android.systemui.qs.pipeline.dagger.QSAutoAddLog
+import com.android.systemui.qs.pipeline.dagger.QSRestoreLog
 import com.android.systemui.qs.pipeline.dagger.QSTileListLog
+import com.android.systemui.qs.pipeline.data.model.RestoreData
+import com.android.systemui.qs.pipeline.data.repository.UserTileSpecRepository
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import javax.inject.Inject
 
@@ -34,11 +36,13 @@
 constructor(
     @QSTileListLog private val tileListLogBuffer: LogBuffer,
     @QSAutoAddLog private val tileAutoAddLogBuffer: LogBuffer,
+    @QSRestoreLog private val restoreLogBuffer: LogBuffer,
 ) {
 
     companion object {
         const val TILE_LIST_TAG = "QSTileListLog"
         const val AUTO_ADD_TAG = "QSAutoAddableLog"
+        const val RESTORE_TAG = "QSRestoreLog"
     }
 
     /**
@@ -60,20 +64,37 @@
         )
     }
 
-    /**
-     * Logs when the tiles change in Settings.
-     *
-     * This could be caused by SystemUI, or restore.
-     */
-    fun logTilesChangedInSettings(newTiles: String, @UserIdInt user: Int) {
+    fun logTilesRestoredAndReconciled(
+        currentTiles: List<TileSpec>,
+        reconciledTiles: List<TileSpec>,
+        user: Int,
+    ) {
         tileListLogBuffer.log(
             TILE_LIST_TAG,
-            LogLevel.VERBOSE,
+            LogLevel.DEBUG,
             {
-                str1 = newTiles
+                str1 = currentTiles.toString()
+                str2 = reconciledTiles.toString()
                 int1 = user
             },
-            { "Tiles changed in settings for user $int1: $str1" }
+            { "Tiles restored and reconciled for user: $int1\nWas: $str1\nSet to: $str2" }
+        )
+    }
+
+    fun logProcessTileChange(
+        action: UserTileSpecRepository.ChangeAction,
+        newList: List<TileSpec>,
+        userId: Int,
+    ) {
+        tileListLogBuffer.log(
+            TILE_LIST_TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = action.toString()
+                str2 = newList.toString()
+                int1 = userId
+            },
+            { "Processing $str1 for user $int1\nNew list: $str2" }
         )
     }
 
@@ -139,6 +160,30 @@
         )
     }
 
+    fun logAutoAddTilesParsed(userId: Int, tiles: Set<TileSpec>) {
+        tileAutoAddLogBuffer.log(
+            AUTO_ADD_TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = tiles.toString()
+                int1 = userId
+            },
+            { "Auto add tiles parsed for user $int1: $str1" }
+        )
+    }
+
+    fun logAutoAddTilesRestoredReconciled(userId: Int, tiles: Set<TileSpec>) {
+        tileAutoAddLogBuffer.log(
+            AUTO_ADD_TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = tiles.toString()
+                int1 = userId
+            },
+            { "Auto-add tiles reconciled for user $int1: $str1" }
+        )
+    }
+
     fun logTileAutoAdded(userId: Int, spec: TileSpec, position: Int) {
         tileAutoAddLogBuffer.log(
             AUTO_ADD_TAG,
@@ -164,6 +209,23 @@
         )
     }
 
+    fun logSettingsRestored(restoreData: RestoreData) {
+        restoreLogBuffer.log(
+            RESTORE_TAG,
+            LogLevel.DEBUG,
+            {
+                int1 = restoreData.userId
+                str1 = restoreData.restoredTiles.toString()
+                str2 = restoreData.restoredAutoAddedTiles.toString()
+            },
+            {
+                "Restored settings data for user $int1\n" +
+                    "\tRestored tiles: $str1\n" +
+                    "\tRestored auto added tiles: $str2"
+            }
+        )
+    }
+
     /** Reasons for destroying an existing tile. */
     enum class TileDestroyedReason(val readable: String) {
         TILE_REMOVED("Tile removed from  current set"),
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index f08eb14..4b3bd0b 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -53,9 +53,6 @@
 import android.graphics.Insets;
 import android.graphics.Rect;
 import android.hardware.display.DisplayManager;
-import android.media.AudioAttributes;
-import android.media.AudioSystem;
-import android.media.MediaPlayer;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Process;
@@ -86,8 +83,6 @@
 import android.window.OnBackInvokedDispatcher;
 import android.window.WindowContext;
 
-import androidx.concurrent.futures.CallbackToFutureAdapter;
-
 import com.android.internal.app.ChooserActivity;
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.policy.PhoneWindow;
@@ -108,7 +103,6 @@
 import dagger.assisted.AssistedFactory;
 import dagger.assisted.AssistedInject;
 
-import java.io.File;
 import java.util.List;
 import java.util.concurrent.CancellationException;
 import java.util.concurrent.ExecutionException;
@@ -116,11 +110,11 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
 import java.util.function.Consumer;
 import java.util.function.Supplier;
 
+import javax.inject.Provider;
+
 
 /**
  * Controls the state and flow for screenshots.
@@ -274,7 +268,8 @@
     private final WindowManager mWindowManager;
     private final WindowManager.LayoutParams mWindowLayoutParams;
     private final AccessibilityManager mAccessibilityManager;
-    private final ListenableFuture<MediaPlayer> mCameraSound;
+    @Nullable
+    private final ScreenshotSoundController mScreenshotSoundController;
     private final ScrollCaptureClient mScrollCaptureClient;
     private final PhoneWindow mWindow;
     private final DisplayManager mDisplayManager;
@@ -339,6 +334,7 @@
             UserManager userManager,
             AssistContentRequester assistContentRequester,
             MessageContainerController messageContainerController,
+            Provider<ScreenshotSoundController> screenshotSoundController,
             @Assisted int displayId
     ) {
         mScreenshotSmartActions = screenshotSmartActions;
@@ -387,8 +383,12 @@
         mConfigChanges.applyNewConfig(context.getResources());
         reloadAssets();
 
-        // Setup the Camera shutter sound
-        mCameraSound = loadCameraSound();
+        // Sound is only reproduced from the controller of the default display.
+        if (displayId == Display.DEFAULT_DISPLAY) {
+            mScreenshotSoundController = screenshotSoundController.get();
+        } else {
+            mScreenshotSoundController = null;
+        }
 
         mCopyBroadcastReceiver = new BroadcastReceiver() {
             @Override
@@ -573,17 +573,8 @@
     }
 
     private void releaseMediaPlayer() {
-        // Note that this may block if the sound is still being loaded (very unlikely) but we can't
-        // reliably release in the background because the service is being destroyed.
-        try {
-            MediaPlayer player = mCameraSound.get(1, TimeUnit.SECONDS);
-            if (player != null) {
-                player.release();
-            }
-        } catch (InterruptedException | ExecutionException | TimeoutException e) {
-            mCameraSound.cancel(true);
-            Log.w(TAG, "Error releasing shutter sound", e);
-        }
+        if (mScreenshotSoundController == null) return;
+        mScreenshotSoundController.releaseScreenshotSound();
     }
 
     private void respondToKeyDismissal() {
@@ -889,39 +880,10 @@
         }
     }
 
-    private ListenableFuture<MediaPlayer> loadCameraSound() {
-        // The media player creation is slow and needs on the background thread.
-        return CallbackToFutureAdapter.getFuture((completer) -> {
-            mBgExecutor.execute(() -> {
-                try {
-                    MediaPlayer player = MediaPlayer.create(mContext,
-                            Uri.fromFile(new File(mContext.getResources().getString(
-                                    com.android.internal.R.string.config_cameraShutterSound))),
-                            null,
-                            new AudioAttributes.Builder()
-                                    .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
-                                    .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
-                                    .build(), AudioSystem.newAudioSessionId());
-                    completer.set(player);
-                } catch (IllegalStateException e) {
-                    Log.w(TAG, "Screenshot sound initialization failed", e);
-                    completer.set(null);
-                }
-            });
-            return "ScreenshotController#loadCameraSound";
-        });
-    }
-
-    private void playCameraSound() {
-        mCameraSound.addListener(() -> {
-            try {
-                MediaPlayer player = mCameraSound.get();
-                if (player != null) {
-                    player.start();
-                }
-            } catch (InterruptedException | ExecutionException e) {
-            }
-        }, mBgExecutor);
+    private void playCameraSoundIfNeeded() {
+        if (mScreenshotSoundController == null) return;
+        // the controller is not-null only on the default display controller
+        mScreenshotSoundController.playCameraSound();
     }
 
     /**
@@ -930,7 +892,7 @@
      */
     private void saveScreenshotAndToast(UserHandle owner, Consumer<Uri> finisher) {
         // Play the shutter sound to notify that we've taken a screenshot
-        playCameraSound();
+        playCameraSoundIfNeeded();
 
         saveScreenshotInWorkerThread(
                 owner,
@@ -974,7 +936,7 @@
         }
 
         // Play the shutter sound to notify that we've taken a screenshot
-        playCameraSound();
+        playCameraSoundIfNeeded();
 
         if (DEBUG_ANIM) {
             Log.d(TAG, "starting post-screenshot animation");
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSoundController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSoundController.kt
new file mode 100644
index 0000000..cd0cab5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSoundController.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.screenshot
+
+import android.media.MediaPlayer
+import android.util.Log
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.util.TraceUtils.Companion.tracedAsync
+import com.google.errorprone.annotations.CanIgnoreReturnValue
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.withTimeout
+
+/** Controls sound reproduction after a screenshot is taken. */
+interface ScreenshotSoundController {
+    /** Reproduces the camera sound. */
+    @CanIgnoreReturnValue fun playCameraSound(): Deferred<Unit>
+
+    /** Releases the sound. [playCameraSound] behaviour is undefined after this has been called. */
+    @CanIgnoreReturnValue fun releaseScreenshotSound(): Deferred<Unit>
+}
+
+class ScreenshotSoundControllerImpl
+@Inject
+constructor(
+    private val soundProvider: ScreenshotSoundProvider,
+    @Application private val coroutineScope: CoroutineScope,
+    @Background private val bgDispatcher: CoroutineDispatcher
+) : ScreenshotSoundController {
+
+    val player: Deferred<MediaPlayer?> =
+        coroutineScope.tracedAsync("loadCameraSound", bgDispatcher) {
+            try {
+                soundProvider.getScreenshotSound()
+            } catch (e: IllegalStateException) {
+                Log.w(TAG, "Screenshot sound initialization failed", e)
+                null
+            }
+        }
+
+    override fun playCameraSound(): Deferred<Unit> {
+        return coroutineScope.tracedAsync("playCameraSound", bgDispatcher) {
+            player.await()?.start()
+        }
+    }
+    override fun releaseScreenshotSound(): Deferred<Unit> {
+        return coroutineScope.tracedAsync("releaseScreenshotSound", bgDispatcher) {
+            try {
+                withTimeout(1.seconds) { player.await()?.release() }
+            } catch (e: TimeoutCancellationException) {
+                player.cancel()
+                Log.w(TAG, "Error releasing shutter sound", e)
+            }
+        }
+    }
+
+    private companion object {
+        const val TAG = "ScreenshotSoundControllerImpl"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSoundProvider.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSoundProvider.kt
new file mode 100644
index 0000000..73151d5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSoundProvider.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.screenshot
+
+import android.content.Context
+import android.media.AudioAttributes
+import android.media.AudioSystem
+import android.media.MediaPlayer
+import android.net.Uri
+import com.android.internal.R
+import com.android.systemui.dagger.SysUISingleton
+import java.io.File
+import javax.inject.Inject
+
+/** Provides a [MediaPlayer] that reproduces the screenshot sound. */
+interface ScreenshotSoundProvider {
+
+    /**
+     * Creates a new [MediaPlayer] that reproduces the screenshot sound. This should be called from
+     * a background thread, as it might take time.
+     */
+    fun getScreenshotSound(): MediaPlayer
+}
+
+@SysUISingleton
+class ScreenshotSoundProviderImpl
+@Inject
+constructor(
+    private val context: Context,
+) : ScreenshotSoundProvider {
+    override fun getScreenshotSound(): MediaPlayer {
+        return MediaPlayer.create(
+            context,
+            Uri.fromFile(File(context.resources.getString(R.string.config_cameraShutterSound))),
+            /* holder = */ null,
+            AudioAttributes.Builder()
+                .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
+                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+                .build(),
+            AudioSystem.newAudioSessionId()
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
index 7d17d4c..3797b8b 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
@@ -25,6 +25,10 @@
 import com.android.systemui.screenshot.ScreenshotPolicyImpl;
 import com.android.systemui.screenshot.ScreenshotProxyService;
 import com.android.systemui.screenshot.ScreenshotRequestProcessor;
+import com.android.systemui.screenshot.ScreenshotSoundController;
+import com.android.systemui.screenshot.ScreenshotSoundControllerImpl;
+import com.android.systemui.screenshot.ScreenshotSoundProvider;
+import com.android.systemui.screenshot.ScreenshotSoundProviderImpl;
 import com.android.systemui.screenshot.TakeScreenshotService;
 import com.android.systemui.screenshot.appclips.AppClipsScreenshotHelperService;
 import com.android.systemui.screenshot.appclips.AppClipsService;
@@ -69,4 +73,12 @@
     @Binds
     abstract ScreenshotRequestProcessor bindScreenshotRequestProcessor(
             RequestProcessor requestProcessor);
+
+    @Binds
+    abstract ScreenshotSoundProvider bindScreenshotSoundProvider(
+            ScreenshotSoundProviderImpl screenshotSoundProviderImpl);
+
+    @Binds
+    abstract ScreenshotSoundController bindScreenshotSoundController(
+            ScreenshotSoundControllerImpl screenshotSoundProviderImpl);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
index e8be40e..9b74ac4 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
@@ -791,7 +791,8 @@
     /** update Qs height state */
     public void setExpansionHeight(float height) {
         // TODO(b/277909752): remove below log when bug is fixed
-        if (mSplitShadeEnabled && mShadeExpandedFraction == 1.0f && height == 0) {
+        if (mSplitShadeEnabled && mShadeExpandedFraction == 1.0f && height == 0
+                && mBarState == SHADE) {
             Log.wtf(TAG,
                     "setting QS height to 0 in split shade while shade is open(ing). "
                             + "Value of mExpandImmediate = " + mExpandImmediate);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 670fb12..93bb435 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -172,6 +172,7 @@
     private static final int MSG_LOCK_TASK_MODE_CHANGED = 75 << MSG_SHIFT;
     private static final int MSG_CONFIRM_IMMERSIVE_PROMPT = 77 << MSG_SHIFT;
     private static final int MSG_IMMERSIVE_CHANGED = 78 << MSG_SHIFT;
+    private static final int MSG_SET_QS_TILES = 79 << MSG_SHIFT;
     public static final int FLAG_EXCLUDE_NONE = 0;
     public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0;
     public static final int FLAG_EXCLUDE_RECENTS_PANEL = 1 << 1;
@@ -301,6 +302,8 @@
 
         default void addQsTile(ComponentName tile) { }
         default void remQsTile(ComponentName tile) { }
+
+        default void setQsTiles(String[] tiles) {}
         default void clickTile(ComponentName tile) { }
 
         default void handleSystemKey(KeyEvent arg1) { }
@@ -903,6 +906,13 @@
     }
 
     @Override
+    public void setQsTiles(String[] tiles) {
+        synchronized (mLock) {
+            mHandler.obtainMessage(MSG_SET_QS_TILES, tiles).sendToTarget();
+        }
+    }
+
+    @Override
     public void clickQsTile(ComponentName tile) {
         synchronized (mLock) {
             mHandler.obtainMessage(MSG_CLICK_QS_TILE, tile).sendToTarget();
@@ -1533,6 +1543,11 @@
                         mCallbacks.get(i).remQsTile((ComponentName) msg.obj);
                     }
                     break;
+                case MSG_SET_QS_TILES:
+                    for (int i = 0; i < mCallbacks.size(); i++) {
+                        mCallbacks.get(i).setQsTiles((String[]) msg.obj);
+                    }
+                    break;
                 case MSG_CLICK_QS_TILE:
                     for (int i = 0; i < mCallbacks.size(); i++) {
                         mCallbacks.get(i).clickTile((ComponentName) msg.obj);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
index 485ab32..f7ff39c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
@@ -75,6 +75,7 @@
 
 import dagger.Lazy;
 
+import java.util.Arrays;
 import java.util.Optional;
 
 import javax.inject.Inject;
@@ -201,6 +202,11 @@
     }
 
     @Override
+    public void setQsTiles(String[] tiles) {
+        mQSHost.changeTilesByUser(mQSHost.getSpecs(), Arrays.stream(tiles).toList());
+    }
+
+    @Override
     public void clickTile(ComponentName tile) {
         // Can't inject this because it changes with the QS fragment
         QSPanelController qsPanelController = mCentralSurfaces.getQSPanelController();
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 6f69ea81..05beded 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -24,9 +24,11 @@
 import static android.view.WindowInsetsController.APPEARANCE_LOW_PROFILE_BARS;
 import static android.view.WindowInsetsController.APPEARANCE_OPAQUE_STATUS_BARS;
 import static android.view.WindowInsetsController.APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS;
+
 import static androidx.core.view.ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
 import static androidx.core.view.ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS;
 import static androidx.lifecycle.Lifecycle.State.RESUMED;
+
 import static com.android.systemui.Dependency.TIME_TICK_HANDLER_NAME;
 import static com.android.systemui.charging.WirelessChargingAnimation.UNKNOWN_BATTERY_LEVEL;
 import static com.android.systemui.statusbar.NotificationLockscreenUserManager.PERMISSION_SELF;
@@ -121,7 +123,6 @@
 import com.android.systemui.EventLogTags;
 import com.android.systemui.InitController;
 import com.android.systemui.Prefs;
-import com.android.systemui.res.R;
 import com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuController;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.assist.AssistManager;
@@ -165,8 +166,9 @@
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.power.domain.interactor.PowerInteractor;
-import com.android.systemui.qs.QSFragment;
+import com.android.systemui.qs.QSFragmentLegacy;
 import com.android.systemui.qs.QSPanelController;
+import com.android.systemui.res.R;
 import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor;
 import com.android.systemui.scrim.ScrimView;
 import com.android.systemui.settings.UserTracker;
@@ -244,8 +246,6 @@
 import com.android.wm.shell.startingsurface.SplashscreenContentDrawer;
 import com.android.wm.shell.startingsurface.StartingSurface;
 
-import dagger.Lazy;
-
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.List;
@@ -257,6 +257,8 @@
 import javax.inject.Named;
 import javax.inject.Provider;
 
+import dagger.Lazy;
+
 /**
  * A class handling initialization and coordination between some of the key central surfaces in
  * System UI: The notification shade, the keyguard (lockscreen), and the status bar.
@@ -1346,9 +1348,10 @@
                     });
             fragmentHostManager.addTagListener(QS.TAG, (tag, f) -> {
                 QS qs = (QS) f;
-                if (qs instanceof QSFragment) {
-                    mQSPanelController = ((QSFragment) qs).getQSPanelController();
-                    ((QSFragment) qs).setBrightnessMirrorController(mBrightnessMirrorController);
+                if (qs instanceof QSFragmentLegacy) {
+                    QSFragmentLegacy qsFragment = (QSFragmentLegacy) qs;
+                    mQSPanelController = qsFragment.getQSPanelController();
+                    qsFragment.setBrightnessMirrorController(mBrightnessMirrorController);
                 }
             });
         }
@@ -1502,7 +1505,7 @@
     protected QS createDefaultQSFragment() {
         return mFragmentService
                 .getFragmentHostManager(getNotificationShadeWindowView())
-                .create(QSFragment.class);
+                .create(QSFragmentLegacy.class);
     }
 
     private void setUpPresenter() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
index b53939e..a27e67b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
@@ -112,6 +112,9 @@
         mDisplayCutout = null;
     }
 
+    // Per b/300629388, we let the PhoneStatusBarView detect onConfigurationChanged to
+    // updateResources, instead of letting the PhoneStatusBarViewController detect onConfigChanged
+    // then notify PhoneStatusBarView.
     @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
index e1096e2..f9702dd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
@@ -16,7 +16,6 @@
 package com.android.systemui.statusbar.phone
 
 import android.app.StatusBarManager.WINDOW_STATUS_BAR
-import android.content.res.Configuration
 import android.graphics.Point
 import android.util.Log
 import android.view.MotionEvent
@@ -72,10 +71,6 @@
 
     private val configurationListener =
         object : ConfigurationController.ConfigurationListener {
-            override fun onConfigChanged(newConfig: Configuration?) {
-                mView.updateResources()
-            }
-
             override fun onDensityOrFontScaleChanged() {
                 mView.onDensityOrFontScaleChanged()
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt
index 02473f2..aacdc63 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt
@@ -186,6 +186,10 @@
             }
         )
     }
+
+    fun logOnSimStateChanged() {
+        buffer.log(TAG, LogLevel.INFO, "onSimStateChanged")
+    }
 }
 
 private const val TAG = "MobileInputLog"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
index ea77163..cf1c97c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
@@ -90,4 +90,12 @@
 
     /** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */
     val defaultMobileIconGroup: Flow<MobileIconGroup>
+
+    /**
+     * If any active SIM on the device is in
+     * [android.telephony.TelephonyManager.SIM_STATE_PIN_REQUIRED] or
+     * [android.telephony.TelephonyManager.SIM_STATE_PUK_REQUIRED] or
+     * [android.telephony.TelephonyManager.SIM_STATE_PERM_DISABLED]
+     */
+    val isAnySimSecure: Flow<Boolean>
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt
index 991ff56..2291631 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt
@@ -151,6 +151,8 @@
     override val defaultMobileIconGroup: Flow<SignalIcon.MobileIconGroup> =
         activeRepo.flatMapLatest { it.defaultMobileIconGroup }
 
+    override val isAnySimSecure: Flow<Boolean> = activeRepo.flatMapLatest { it.isAnySimSecure }
+
     override val defaultDataSubId: StateFlow<Int> =
         activeRepo
             .flatMapLatest { it.defaultDataSubId }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
index ee13d93..c7987e2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
@@ -134,6 +134,8 @@
 
     override val defaultMobileIconGroup = flowOf(TelephonyIcons.THREE_G)
 
+    override val isAnySimSecure: Flow<Boolean> = flowOf(false)
+
     override val defaultMobileIconMapping = MutableStateFlow(TelephonyIcons.ICON_NAME_TO_ICON)
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
index ec54f08..ecb80f2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
@@ -28,9 +28,10 @@
 import android.telephony.TelephonyManager
 import androidx.annotation.VisibleForTesting
 import com.android.internal.telephony.PhoneConstants
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.settingslib.SignalIcon.MobileIconGroup
 import com.android.settingslib.mobile.MobileMappings.Config
-import com.android.systemui.res.R
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
@@ -38,6 +39,7 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.log.table.logDiffsForTable
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
 import com.android.systemui.statusbar.pipeline.dagger.MobileSummaryLog
 import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger
@@ -94,6 +96,7 @@
     // See [CarrierMergedConnectionRepository] for details.
     wifiRepository: WifiRepository,
     private val fullMobileRepoFactory: FullMobileConnectionRepository.Factory,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
 ) : MobileConnectionsRepository {
     private var subIdRepositoryCache: MutableMap<Int, FullMobileConnectionRepository> =
         mutableMapOf()
@@ -253,6 +256,27 @@
             .distinctUntilChanged()
             .onEach { logger.logDefaultMobileIconGroup(it) }
 
+    override val isAnySimSecure: Flow<Boolean> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : KeyguardUpdateMonitorCallback() {
+                        override fun onSimStateChanged(subId: Int, slotId: Int, simState: Int) {
+                            logger.logOnSimStateChanged()
+                            trySend(keyguardUpdateMonitor.isSimPinSecure)
+                        }
+                    }
+                keyguardUpdateMonitor.registerCallback(callback)
+                trySend(false)
+                awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
+            }
+            .logDiffsForTable(
+                tableLogger,
+                LOGGING_PREFIX,
+                columnName = "isAnySimSecure",
+                initialValue = false,
+            )
+            .distinctUntilChanged()
+
     override fun getRepoForSubId(subId: Int): FullMobileConnectionRepository =
         getOrCreateRepoForSubId(subId)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
index 360fa56..944b059 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
@@ -32,7 +32,6 @@
 import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT
 import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT
 import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN
-import com.android.systemui.res.R
 import com.android.systemui.RoboPilotTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.AuthController
@@ -48,6 +47,10 @@
 import com.android.systemui.keyguard.data.repository.BiometricType.SIDE_FINGERPRINT
 import com.android.systemui.keyguard.data.repository.BiometricType.UNDER_DISPLAY_FINGERPRINT
 import com.android.systemui.keyguard.shared.model.DevicePosture
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.statusbar.policy.DevicePostureController
 import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.util.mockito.eq
@@ -87,6 +90,7 @@
     @Mock private lateinit var devicePolicyManager: DevicePolicyManager
     @Mock private lateinit var dumpManager: DumpManager
     @Mock private lateinit var biometricManager: BiometricManager
+    @Mock private lateinit var tableLogger: TableLogBuffer
     @Captor
     private lateinit var strongAuthTracker: ArgumentCaptor<LockPatternUtils.StrongAuthTracker>
     @Captor private lateinit var authControllerCallback: ArgumentCaptor<AuthController.Callback>
@@ -97,6 +101,7 @@
     private lateinit var devicePostureRepository: FakeDevicePostureRepository
     private lateinit var facePropertyRepository: FakeFacePropertyRepository
     private lateinit var fingerprintPropertyRepository: FakeFingerprintPropertyRepository
+    private lateinit var mobileConnectionsRepository: FakeMobileConnectionsRepository
 
     private lateinit var testDispatcher: TestDispatcher
     private lateinit var testScope: TestScope
@@ -112,6 +117,8 @@
         devicePostureRepository = FakeDevicePostureRepository()
         facePropertyRepository = FakeFacePropertyRepository()
         fingerprintPropertyRepository = FakeFingerprintPropertyRepository()
+        mobileConnectionsRepository =
+            FakeMobileConnectionsRepository(FakeMobileMappingsProxy(), tableLogger)
     }
 
     private suspend fun createBiometricSettingsRepository() {
@@ -132,6 +139,7 @@
                 dumpManager = dumpManager,
                 facePropertyRepository = facePropertyRepository,
                 fingerprintPropertyRepository = fingerprintPropertyRepository,
+                mobileConnectionsRepository = mobileConnectionsRepository,
             )
         testScope.runCurrent()
         fingerprintPropertyRepository.setProperties(
@@ -421,6 +429,50 @@
         }
 
     @Test
+    fun anySimSecure_disablesFaceAuth() =
+        testScope.runTest {
+            faceAuthIsEnrolled()
+            createBiometricSettingsRepository()
+
+            faceAuthIsEnabledByBiometricManager()
+            doNotDisableKeyguardAuthFeatures()
+            mobileConnectionsRepository.isAnySimSecure.value = false
+            runCurrent()
+
+            val isFaceAuthEnabledAndEnrolled by
+                collectLastValue(underTest.isFaceAuthEnrolledAndEnabled)
+
+            assertThat(isFaceAuthEnabledAndEnrolled).isTrue()
+
+            mobileConnectionsRepository.isAnySimSecure.value = true
+            runCurrent()
+
+            assertThat(isFaceAuthEnabledAndEnrolled).isFalse()
+        }
+
+    @Test
+    fun anySimSecure_disablesFaceAuthToNotCurrentlyRun() =
+        testScope.runTest {
+            faceAuthIsEnrolled()
+
+            createBiometricSettingsRepository()
+            val isFaceAuthCurrentlyAllowed by collectLastValue(underTest.isFaceAuthCurrentlyAllowed)
+
+            deviceIsInPostureThatSupportsFaceAuth()
+            doNotDisableKeyguardAuthFeatures()
+            faceAuthIsStrongBiometric()
+            faceAuthIsEnabledByBiometricManager()
+            mobileConnectionsRepository.isAnySimSecure.value = false
+
+            onStrongAuthChanged(STRONG_AUTH_NOT_REQUIRED, PRIMARY_USER_ID)
+            onNonStrongAuthChanged(false, PRIMARY_USER_ID)
+            assertThat(isFaceAuthCurrentlyAllowed).isTrue()
+
+            mobileConnectionsRepository.isAnySimSecure.value = true
+            assertThat(isFaceAuthCurrentlyAllowed).isFalse()
+        }
+
+    @Test
     fun biometricManagerControlsFaceAuthenticationEnabledStatus() =
         testScope.runTest {
             faceAuthIsEnrolled()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSDisableFlagsLoggerTest.kt
similarity index 72%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/qs/QSDisableFlagsLoggerTest.kt
index 93f316e..9e5d3bd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSDisableFlagsLoggerTest.kt
@@ -29,15 +29,16 @@
 import org.mockito.Mockito.mock
 
 @SmallTest
-class QSFragmentDisableFlagsLoggerTest : SysuiTestCase() {
+class QSDisableFlagsLoggerTest : SysuiTestCase() {
 
-    private val buffer = LogBufferFactory(DumpManager(), mock(LogcatEchoTracker::class.java))
-        .create("buffer", 10)
-    private val disableFlagsLogger = DisableFlagsLogger(
-        listOf(DisableFlagsLogger.DisableFlag(0b001, 'A', 'a')),
-        listOf(DisableFlagsLogger.DisableFlag(0b001, 'B', 'b'))
-    )
-    private val logger = QSFragmentDisableFlagsLogger(buffer, disableFlagsLogger)
+    private val buffer =
+        LogBufferFactory(DumpManager(), mock(LogcatEchoTracker::class.java)).create("buffer", 10)
+    private val disableFlagsLogger =
+        DisableFlagsLogger(
+            listOf(DisableFlagsLogger.DisableFlag(0b001, 'A', 'a')),
+            listOf(DisableFlagsLogger.DisableFlag(0b001, 'B', 'b'))
+        )
+    private val logger = QSDisableFlagsLogger(buffer, disableFlagsLogger)
 
     @Test
     fun logDisableFlagChange_bufferHasStates() {
@@ -48,9 +49,8 @@
         val stringWriter = StringWriter()
         buffer.dump(PrintWriter(stringWriter), tailLength = 0)
         val actualString = stringWriter.toString()
-        val expectedLogString = disableFlagsLogger.getDisableFlagsString(
-            new = state, newAfterLocalModification = state
-        )
+        val expectedLogString =
+            disableFlagsLogger.getDisableFlagsString(new = state, newAfterLocalModification = state)
 
         assertThat(actualString).contains(expectedLogString)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java
similarity index 67%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java
index c4c233c..d57765c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java
@@ -1,15 +1,17 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * 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
+ * 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.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
  */
 
 package com.android.systemui.qs;
@@ -34,7 +36,6 @@
 import static org.mockito.Mockito.when;
 
 import android.annotation.Nullable;
-import android.app.Fragment;
 import android.content.Context;
 import android.graphics.Rect;
 import android.os.Bundle;
@@ -49,12 +50,12 @@
 
 import com.android.keyguard.BouncerPanelExpansionCalculator;
 import com.android.systemui.res.R;
-import com.android.systemui.SysuiBaseFragmentTest;
+import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.FeatureFlagsClassic;
 import com.android.systemui.media.controls.ui.MediaHost;
 import com.android.systemui.qs.customize.QSCustomizerController;
-import com.android.systemui.qs.dagger.QSFragmentComponent;
+import com.android.systemui.qs.dagger.QSComponent;
 import com.android.systemui.qs.external.TileServiceRequestController;
 import com.android.systemui.qs.footer.ui.binder.FooterActionsViewBinder;
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel;
@@ -66,8 +67,8 @@
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController;
 import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
+import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController;
 import com.android.systemui.util.animation.UniqueObjectHostView;
 
 import org.junit.Before;
@@ -79,10 +80,9 @@
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper(setAsMainLooper = true)
 @SmallTest
-public class QSFragmentTest extends SysuiBaseFragmentTest {
+public class QSImplTest extends SysuiTestCase {
 
-    @Mock private QSFragmentComponent.Factory mQsComponentFactory;
-    @Mock private QSFragmentComponent mQsFragmentComponent;
+    @Mock private QSComponent mQsComponent;
     @Mock private QSPanelController mQSPanelController;
     @Mock private MediaHost mQSMediaHost;
     @Mock private MediaHost mQQSMediaHost;
@@ -107,69 +107,54 @@
     @Mock private FooterActionsViewModel.Factory mFooterActionsViewModelFactory;
     @Mock private FooterActionsViewBinder mFooterActionsViewBinder;
     @Mock private LargeScreenShadeInterpolator mLargeScreenShadeInterpolator;
-    @Mock private FeatureFlags mFeatureFlags;
-    private View mQsFragmentView;
+    @Mock private FeatureFlagsClassic mFeatureFlags;
+    private View mQsView;
 
-    public QSFragmentTest() {
-        super(QSFragment.class);
-    }
+    private QSImpl mUnderTest;
+
 
     @Before
     public void setup() {
-        injectLeakCheckedDependencies(ALL_SUPPORTED_CLASSES);
+        mUnderTest = instantiate();
+
+        mUnderTest.onComponentCreated(mQsComponent, null);
     }
 
-    @Test
-    public void testListening() {
-        QSFragment qs = (QSFragment) mFragment;
-        mFragments.dispatchResume();
-        processAllMessages();
-
-        qs.setListening(true);
-        processAllMessages();
-
-        qs.setListening(false);
-        processAllMessages();
-    }
 
     @Test
     public void testSaveState() {
-        mFragments.dispatchResume();
-        processAllMessages();
+        mUnderTest.setListening(true);
+        mUnderTest.setExpanded(true);
+        mUnderTest.setQsVisible(true);
 
-        QSFragment qs = (QSFragment) mFragment;
-        qs.setListening(true);
-        qs.setExpanded(true);
-        qs.setQsVisible(true);
-        processAllMessages();
-        recreateFragment();
-        processAllMessages();
+        Bundle bundle = new Bundle();
+        mUnderTest.onSaveInstanceState(bundle);
 
-        // Get the reference to the new fragment.
-        qs = (QSFragment) mFragment;
-        assertTrue(qs.isListening());
-        assertTrue(qs.isExpanded());
-        assertTrue(qs.isQsVisible());
+        // Get a new instance
+        QSImpl other = instantiate();
+        other.onComponentCreated(mQsComponent, bundle);
+
+        assertTrue(other.isListening());
+        assertTrue(other.isExpanded());
+        assertTrue(other.isQsVisible());
     }
 
     @Test
     public void transitionToFullShade_smallScreen_alphaAlways1() {
-        QSFragment fragment = resumeAndGetFragment();
         setIsSmallScreen();
         setStatusBarCurrentAndUpcomingState(StatusBarState.SHADE);
         boolean isTransitioningToFullShade = true;
         float transitionProgress = 0.5f;
         float squishinessFraction = 0.5f;
 
-        fragment.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress,
+        mUnderTest.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress,
                 squishinessFraction);
 
-        assertThat(mQsFragmentView.getAlpha()).isEqualTo(1f);
+        assertThat(mQsView.getAlpha()).isEqualTo(1f);
     }
 
     @Test
     public void transitionToFullShade_largeScreen_alphaLargeScreenShadeInterpolator() {
-        QSFragment fragment = resumeAndGetFragment();
         setIsLargeScreen();
         setStatusBarCurrentAndUpcomingState(StatusBarState.SHADE);
         boolean isTransitioningToFullShade = true;
@@ -177,43 +162,40 @@
         float squishinessFraction = 0.5f;
         when(mLargeScreenShadeInterpolator.getQsAlpha(transitionProgress)).thenReturn(123f);
 
-        fragment.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress,
+        mUnderTest.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress,
                 squishinessFraction);
 
-        assertThat(mQsFragmentView.getAlpha())
-                .isEqualTo(123f);
+        assertThat(mQsView.getAlpha()).isEqualTo(123f);
     }
 
     @Test
     public void
             transitionToFullShade_onKeyguard_noBouncer_setsAlphaUsingLinearInterpolator() {
-        QSFragment fragment = resumeAndGetFragment();
         setStatusBarCurrentAndUpcomingState(KEYGUARD);
         when(mQSPanelController.isBouncerInTransit()).thenReturn(false);
         boolean isTransitioningToFullShade = true;
         float transitionProgress = 0.5f;
         float squishinessFraction = 0.5f;
 
-        fragment.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress,
+        mUnderTest.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress,
                 squishinessFraction);
 
-        assertThat(mQsFragmentView.getAlpha()).isEqualTo(transitionProgress);
+        assertThat(mQsView.getAlpha()).isEqualTo(transitionProgress);
     }
 
     @Test
     public void
             transitionToFullShade_onKeyguard_bouncerActive_setsAlphaUsingBouncerInterpolator() {
-        QSFragment fragment = resumeAndGetFragment();
         setStatusBarCurrentAndUpcomingState(KEYGUARD);
         when(mQSPanelController.isBouncerInTransit()).thenReturn(true);
         boolean isTransitioningToFullShade = true;
         float transitionProgress = 0.5f;
         float squishinessFraction = 0.5f;
 
-        fragment.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress,
+        mUnderTest.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress,
                 squishinessFraction);
 
-        assertThat(mQsFragmentView.getAlpha())
+        assertThat(mQsView.getAlpha())
                 .isEqualTo(
                         BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(
                                 transitionProgress));
@@ -221,40 +203,37 @@
 
     @Test
     public void transitionToFullShade_inFullWidth_alwaysSetsAlphaTo1() {
-        QSFragment fragment = resumeAndGetFragment();
-        fragment.setIsNotificationPanelFullWidth(true);
+        mUnderTest.setIsNotificationPanelFullWidth(true);
 
         boolean isTransitioningToFullShade = true;
         float transitionProgress = 0.1f;
         float squishinessFraction = 0.5f;
-        fragment.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress,
+        mUnderTest.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress,
                 squishinessFraction);
-        assertThat(mQsFragmentView.getAlpha()).isEqualTo(1);
+        assertThat(mQsView.getAlpha()).isEqualTo(1);
 
         transitionProgress = 0.5f;
-        fragment.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress,
+        mUnderTest.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress,
                 squishinessFraction);
-        assertThat(mQsFragmentView.getAlpha()).isEqualTo(1);
-        assertThat(mQsFragmentView.getAlpha()).isEqualTo(1);
+        assertThat(mQsView.getAlpha()).isEqualTo(1);
+        assertThat(mQsView.getAlpha()).isEqualTo(1);
 
         transitionProgress = 0.7f;
-        fragment.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress,
+        mUnderTest.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress,
                 squishinessFraction);
-        assertThat(mQsFragmentView.getAlpha()).isEqualTo(1);
+        assertThat(mQsView.getAlpha()).isEqualTo(1);
     }
 
     @Test
     public void transitionToFullShade_setsSquishinessOnController() {
-        QSFragment fragment = resumeAndGetFragment();
         boolean isTransitioningToFullShade = true;
         float transitionProgress = 0.123f;
         float squishinessFraction = 0.456f;
 
-        fragment.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress,
+        mUnderTest.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress,
                 squishinessFraction);
 
-        verify(mQsFragmentComponent.getQSSquishinessController())
-                .setSquishiness(squishinessFraction);
+        verify(mQsComponent.getQSSquishinessController()).setSquishiness(squishinessFraction);
     }
 
     @Test
@@ -265,10 +244,9 @@
         float proposedTranslation = 456f;
         float squishinessFraction = 0.987f;
 
-        QSFragment fragment = resumeAndGetFragment();
         enableSplitShade();
 
-        fragment.setQsExpansion(expansion, panelExpansionFraction, proposedTranslation,
+        mUnderTest.setQsExpansion(expansion, panelExpansionFraction, proposedTranslation,
                 squishinessFraction);
 
         verify(mFooterActionsViewModel).onQuickSettingsExpansionChanged(
@@ -283,10 +261,9 @@
         float proposedTranslation = 456f;
         float squishinessFraction = 0.987f;
 
-        QSFragment fragment = resumeAndGetFragment();
         disableSplitShade();
 
-        fragment.setQsExpansion(expansion, panelExpansionFraction, proposedTranslation,
+        mUnderTest.setQsExpansion(expansion, panelExpansionFraction, proposedTranslation,
                 squishinessFraction);
 
         verify(mFooterActionsViewModel).onQuickSettingsExpansionChanged(
@@ -295,7 +272,6 @@
 
     @Test
     public void setQsExpansion_inSplitShade_whenTransitioningToKeyguard_setsAlphaBasedOnShadeTransitionProgress() {
-        QSFragment fragment = resumeAndGetFragment();
         enableSplitShade();
         when(mStatusBarStateController.getState()).thenReturn(SHADE);
         when(mStatusBarStateController.getCurrentOrUpcomingState()).thenReturn(KEYGUARD);
@@ -303,24 +279,23 @@
         float transitionProgress = 0;
         float squishinessFraction = 0f;
 
-        fragment.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress,
+        mUnderTest.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress,
                 squishinessFraction);
 
         // trigger alpha refresh with non-zero expansion and fraction values
-        fragment.setQsExpansion(/* expansion= */ 1, /* panelExpansionFraction= */1,
+        mUnderTest.setQsExpansion(/* expansion= */ 1, /* panelExpansionFraction= */1,
                 /* proposedTranslation= */ 0, /* squishinessFraction= */ 1);
 
         // alpha should follow lockscreen to shade progress, not panel expansion fraction
-        assertThat(mQsFragmentView.getAlpha()).isEqualTo(transitionProgress);
+        assertThat(mQsView.getAlpha()).isEqualTo(transitionProgress);
     }
 
     @Test
     public void getQsMinExpansionHeight_notInSplitShade_returnsHeaderHeight() {
-        QSFragment fragment = resumeAndGetFragment();
         disableSplitShade();
         when(mHeader.getHeight()).thenReturn(1234);
 
-        int height = fragment.getQsMinExpansionHeight();
+        int height = mUnderTest.getQsMinExpansionHeight();
 
         assertThat(height).isEqualTo(mHeader.getHeight());
     }
@@ -329,13 +304,12 @@
     public void getQsMinExpansionHeight_inSplitShade_returnsAbsoluteBottomOfQSContainer() {
         int top = 1234;
         int height = 9876;
-        QSFragment fragment = resumeAndGetFragment();
         enableSplitShade();
-        setLocationOnScreen(mQsFragmentView, top);
-        when(mQsFragmentView.getHeight()).thenReturn(height);
+        setLocationOnScreen(mQsView, top);
+        when(mQsView.getHeight()).thenReturn(height);
 
         int expectedHeight = top + height;
-        assertThat(fragment.getQsMinExpansionHeight()).isEqualTo(expectedHeight);
+        assertThat(mUnderTest.getQsMinExpansionHeight()).isEqualTo(expectedHeight);
     }
 
     @Test
@@ -343,47 +317,43 @@
         int top = 1234;
         int height = 9876;
         float translationY = -600f;
-        QSFragment fragment = resumeAndGetFragment();
         enableSplitShade();
-        setLocationOnScreen(mQsFragmentView, (int) (top + translationY));
-        when(mQsFragmentView.getHeight()).thenReturn(height);
-        when(mQsFragmentView.getTranslationY()).thenReturn(translationY);
+        setLocationOnScreen(mQsView, (int) (top + translationY));
+        when(mQsView.getHeight()).thenReturn(height);
+        when(mQsView.getTranslationY()).thenReturn(translationY);
 
         int expectedHeight = top + height;
-        assertThat(fragment.getQsMinExpansionHeight()).isEqualTo(expectedHeight);
+        assertThat(mUnderTest.getQsMinExpansionHeight()).isEqualTo(expectedHeight);
     }
 
     @Test
     public void hideImmediately_notInSplitShade_movesViewUpByHeaderHeight() {
-        QSFragment fragment = resumeAndGetFragment();
         disableSplitShade();
         when(mHeader.getHeight()).thenReturn(555);
 
-        fragment.hideImmediately();
+        mUnderTest.hideImmediately();
 
-        assertThat(mQsFragmentView.getY()).isEqualTo(-mHeader.getHeight());
+        assertThat(mQsView.getY()).isEqualTo(-mHeader.getHeight());
     }
 
     @Test
     public void hideImmediately_inSplitShade_movesViewUpByQSAbsoluteBottom() {
-        QSFragment fragment = resumeAndGetFragment();
         enableSplitShade();
         int top = 1234;
         int height = 9876;
-        setLocationOnScreen(mQsFragmentView, top);
-        when(mQsFragmentView.getHeight()).thenReturn(height);
+        setLocationOnScreen(mQsView, top);
+        when(mQsView.getHeight()).thenReturn(height);
 
-        fragment.hideImmediately();
+        mUnderTest.hideImmediately();
 
         int qsAbsoluteBottom = top + height;
-        assertThat(mQsFragmentView.getY()).isEqualTo(-qsAbsoluteBottom);
+        assertThat(mQsView.getY()).isEqualTo(-qsAbsoluteBottom);
     }
 
     @Test
     public void setCollapseExpandAction_passedToControllers() {
         Runnable action = () -> {};
-        QSFragment fragment = resumeAndGetFragment();
-        fragment.setCollapseExpandAction(action);
+        mUnderTest.setCollapseExpandAction(action);
 
         verify(mQSPanelController).setCollapseExpandAction(action);
         verify(mQuickQSPanelController).setCollapseExpandAction(action);
@@ -391,24 +361,21 @@
 
     @Test
     public void setOverScrollAmount_setsTranslationOnView() {
-        QSFragment fragment = resumeAndGetFragment();
+        mUnderTest.setOverScrollAmount(123);
 
-        fragment.setOverScrollAmount(123);
-
-        assertThat(mQsFragmentView.getTranslationY()).isEqualTo(123);
+        assertThat(mQsView.getTranslationY()).isEqualTo(123);
     }
 
     @Test
     public void setOverScrollAmount_beforeViewCreated_translationIsNotSet() {
-        QSFragment fragment = getFragment();
+        QSImpl other = instantiate();
+        other.setOverScrollAmount(123);
 
-        fragment.setOverScrollAmount(123);
-
-        assertThat(mQsFragmentView.getTranslationY()).isEqualTo(0);
+        assertThat(mQsView.getTranslationY()).isEqualTo(0);
     }
 
     private Lifecycle.State getListeningAndVisibilityLifecycleState() {
-        return getFragment()
+        return mUnderTest
                 .getListeningAndVisibilityLifecycleOwner()
                 .getLifecycle()
                 .getCurrentState();
@@ -416,11 +383,10 @@
 
     @Test
     public void setListeningFalse_notVisible() {
-        QSFragment fragment = resumeAndGetFragment();
-        fragment.setQsVisible(false);
+        mUnderTest.setQsVisible(false);
         clearInvocations(mQSContainerImplController, mQSPanelController, mQSFooterActionController);
 
-        fragment.setListening(false);
+        mUnderTest.setListening(false);
         verify(mQSContainerImplController).setListening(false);
         assertThat(getListeningAndVisibilityLifecycleState()).isEqualTo(Lifecycle.State.CREATED);
         verify(mQSPanelController).setListening(eq(false), anyBoolean());
@@ -428,11 +394,10 @@
 
     @Test
     public void setListeningTrue_notVisible() {
-        QSFragment fragment = resumeAndGetFragment();
-        fragment.setQsVisible(false);
+        mUnderTest.setQsVisible(false);
         clearInvocations(mQSContainerImplController, mQSPanelController, mQSFooterActionController);
 
-        fragment.setListening(true);
+        mUnderTest.setListening(true);
         verify(mQSContainerImplController).setListening(false);
         assertThat(getListeningAndVisibilityLifecycleState()).isEqualTo(Lifecycle.State.STARTED);
         verify(mQSPanelController).setListening(eq(false), anyBoolean());
@@ -440,11 +405,10 @@
 
     @Test
     public void setListeningFalse_visible() {
-        QSFragment fragment = resumeAndGetFragment();
-        fragment.setQsVisible(true);
+        mUnderTest.setQsVisible(true);
         clearInvocations(mQSContainerImplController, mQSPanelController, mQSFooterActionController);
 
-        fragment.setListening(false);
+        mUnderTest.setListening(false);
         verify(mQSContainerImplController).setListening(false);
         assertThat(getListeningAndVisibilityLifecycleState()).isEqualTo(Lifecycle.State.CREATED);
         verify(mQSPanelController).setListening(eq(false), anyBoolean());
@@ -452,11 +416,10 @@
 
     @Test
     public void setListeningTrue_visible() {
-        QSFragment fragment = resumeAndGetFragment();
-        fragment.setQsVisible(true);
+        mUnderTest.setQsVisible(true);
         clearInvocations(mQSContainerImplController, mQSPanelController, mQSFooterActionController);
 
-        fragment.setListening(true);
+        mUnderTest.setListening(true);
         verify(mQSContainerImplController).setListening(true);
         assertThat(getListeningAndVisibilityLifecycleState()).isEqualTo(Lifecycle.State.RESUMED);
         verify(mQSPanelController).setListening(eq(true), anyBoolean());
@@ -464,31 +427,28 @@
 
     @Test
     public void passCorrectExpansionState_inSplitShade() {
-        QSFragment fragment = resumeAndGetFragment();
         enableSplitShade();
         clearInvocations(mQSPanelController);
 
-        fragment.setExpanded(true);
+        mUnderTest.setExpanded(true);
         verify(mQSPanelController).setExpanded(true);
 
-        fragment.setExpanded(false);
+        mUnderTest.setExpanded(false);
         verify(mQSPanelController).setExpanded(false);
     }
 
     @Test
     public void startsListeningAfterStateChangeToExpanded_inSplitShade() {
-        QSFragment fragment = resumeAndGetFragment();
         enableSplitShade();
-        fragment.setQsVisible(true);
+        mUnderTest.setQsVisible(true);
         clearInvocations(mQSPanelController);
 
-        fragment.setExpanded(true);
+        mUnderTest.setExpanded(true);
         verify(mQSPanelController).setListening(true, true);
     }
 
     @Test
     public void testUpdateQSBounds_setMediaClipCorrectly() {
-        QSFragment fragment = resumeAndGetFragment();
         disableSplitShade();
 
         Rect mediaHostClip = new Rect();
@@ -497,7 +457,7 @@
         when(mQSPanelScrollView.getMeasuredHeight()).thenReturn(200);
         when(mQSMediaHost.getCurrentClipping()).thenReturn(mediaHostClip);
 
-        fragment.updateQsBounds();
+        mUnderTest.updateQsBounds();
 
         assertEquals(25, mediaHostClip.top);
         assertEquals(175, mediaHostClip.bottom);
@@ -505,17 +465,15 @@
 
     @Test
     public void testQsUpdatesQsAnimatorWithUpcomingState() {
-        QSFragment fragment = resumeAndGetFragment();
         setStatusBarCurrentAndUpcomingState(SHADE);
-        fragment.onUpcomingStateChanged(KEYGUARD);
+        mUnderTest.onUpcomingStateChanged(KEYGUARD);
 
         verify(mQSAnimator).setOnKeyguard(true);
     }
 
-    @Override
-    protected Fragment instantiate(Context context, String className, Bundle arguments) {
+    private QSImpl instantiate() {
         MockitoAnnotations.initMocks(this);
-        CommandQueue commandQueue = new CommandQueue(context, new FakeDisplayTracker(context));
+        CommandQueue commandQueue = new CommandQueue(mContext, new FakeDisplayTracker(mContext));
 
         setupQsComponent();
         setUpViews();
@@ -523,9 +481,9 @@
         setUpMedia();
         setUpOther();
 
-        return new QSFragment(
+        return new QSImpl(
                 new RemoteInputQuickSettingsDisabler(
-                        context,
+                        mContext,
                         commandQueue,
                         new ResourcesSplitShadeStateController(),
                         mock(ConfigurationController.class)),
@@ -534,8 +492,7 @@
                 mQSMediaHost,
                 mQQSMediaHost,
                 mBypassController,
-                mQsComponentFactory,
-                mock(QSFragmentDisableFlagsLogger.class),
+                mock(QSDisableFlagsLogger.class),
                 mock(DumpManager.class),
                 mock(QSLogger.class),
                 mock(FooterActionsController.class),
@@ -561,12 +518,13 @@
     }
 
     private void setUpViews() {
-        mQsFragmentView = spy(new View(mContext));
-        when(mQsFragmentView.findViewById(R.id.expanded_qs_scroll_view))
+        mQsView = spy(new View(mContext));
+        when(mQsComponent.getRootView()).thenReturn(mQsView);
+        when(mQsView.findViewById(R.id.expanded_qs_scroll_view))
                 .thenReturn(mQSPanelScrollView);
-        when(mQsFragmentView.findViewById(R.id.header)).thenReturn(mHeader);
-        when(mQsFragmentView.findViewById(android.R.id.edit)).thenReturn(new View(mContext));
-        when(mQsFragmentView.findViewById(R.id.qs_footer_actions)).thenAnswer(
+        when(mQsView.findViewById(R.id.header)).thenReturn(mHeader);
+        when(mQsView.findViewById(android.R.id.edit)).thenReturn(new View(mContext));
+        when(mQsView.findViewById(R.id.qs_footer_actions)).thenAnswer(
                 invocation -> new FooterActionsViewBinder().create(mContext));
     }
 
@@ -597,37 +555,26 @@
             return realInflater.inflate(layoutRes, root, attachToRoot);
         }
 
-        return mQsFragmentView;
+        return mQsView;
     }
 
     private void setupQsComponent() {
-        when(mQsComponentFactory.create(any(QSFragment.class))).thenReturn(mQsFragmentComponent);
-        when(mQsFragmentComponent.getQSPanelController()).thenReturn(mQSPanelController);
-        when(mQsFragmentComponent.getQuickQSPanelController()).thenReturn(mQuickQSPanelController);
-        when(mQsFragmentComponent.getQSCustomizerController()).thenReturn(mQsCustomizerController);
-        when(mQsFragmentComponent.getQSContainerImplController())
+        when(mQsComponent.getQSPanelController()).thenReturn(mQSPanelController);
+        when(mQsComponent.getQuickQSPanelController()).thenReturn(mQuickQSPanelController);
+        when(mQsComponent.getQSCustomizerController()).thenReturn(mQsCustomizerController);
+        when(mQsComponent.getQSContainerImplController())
                 .thenReturn(mQSContainerImplController);
-        when(mQsFragmentComponent.getQSFooter()).thenReturn(mFooter);
-        when(mQsFragmentComponent.getQSFooterActionController())
+        when(mQsComponent.getQSFooter()).thenReturn(mFooter);
+        when(mQsComponent.getQSFooterActionController())
                 .thenReturn(mQSFooterActionController);
-        when(mQsFragmentComponent.getQSAnimator()).thenReturn(mQSAnimator);
-        when(mQsFragmentComponent.getQSSquishinessController()).thenReturn(mSquishinessController);
-    }
-
-    private QSFragment getFragment() {
-        return ((QSFragment) mFragment);
-    }
-
-    private QSFragment resumeAndGetFragment() {
-        mFragments.dispatchResume();
-        processAllMessages();
-        return getFragment();
+        when(mQsComponent.getQSAnimator()).thenReturn(mQSAnimator);
+        when(mQsComponent.getQSSquishinessController()).thenReturn(mSquishinessController);
     }
 
     private void setStatusBarCurrentAndUpcomingState(int statusBarState) {
         when(mStatusBarStateController.getState()).thenReturn(statusBarState);
         when(mStatusBarStateController.getCurrentOrUpcomingState()).thenReturn(statusBarState);
-        getFragment().onStateChanged(statusBarState);
+        mUnderTest.onStateChanged(statusBarState);
     }
 
     private void enableSplitShade() {
@@ -639,7 +586,7 @@
     }
 
     private void setSplitShadeEnabled(boolean enabled) {
-        getFragment().setInSplitShade(enabled);
+        mUnderTest.setInSplitShade(enabled);
     }
 
     private void setLocationOnScreen(View view, int top) {
@@ -652,10 +599,10 @@
     }
 
     private void setIsLargeScreen() {
-        getFragment().setIsNotificationPanelFullWidth(false);
+        mUnderTest.setIsNotificationPanelFullWidth(false);
     }
 
     private void setIsSmallScreen() {
-        getFragment().setIsNotificationPanelFullWidth(true);
+        mUnderTest.setIsNotificationPanelFullWidth(true);
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/AutoAddSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/AutoAddSettingsRepositoryTest.kt
index 9386d71..9a55f72 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/AutoAddSettingsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/AutoAddSettingsRepositoryTest.kt
@@ -23,15 +23,19 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
 import com.android.systemui.util.settings.FakeSettings
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -39,6 +43,20 @@
 @RunWith(AndroidJUnit4::class)
 class AutoAddSettingsRepositoryTest : SysuiTestCase() {
     private val secureSettings = FakeSettings()
+    private val userAutoAddRepositoryFactory =
+        object : UserAutoAddRepository.Factory {
+            override fun create(userId: Int): UserAutoAddRepository {
+                return UserAutoAddRepository(
+                    userId,
+                    secureSettings,
+                    logger,
+                    testScope.backgroundScope,
+                    testDispatcher,
+                )
+            }
+        }
+
+    @Mock private lateinit var logger: QSPipelineLogger
 
     private val testDispatcher = StandardTestDispatcher()
     private val testScope = TestScope(testDispatcher)
@@ -47,110 +65,37 @@
 
     @Before
     fun setUp() {
-        underTest =
-            AutoAddSettingRepository(
-                secureSettings,
-                testDispatcher,
-            )
+        MockitoAnnotations.initMocks(this)
+
+        underTest = AutoAddSettingRepository(userAutoAddRepositoryFactory)
     }
 
     @Test
-    fun nonExistentSetting_emptySet() =
-        testScope.runTest {
-            val specs by collectLastValue(underTest.autoAddedTiles(0))
-
-            assertThat(specs).isEmpty()
-        }
-
-    @Test
-    fun settingsChange_correctValues() =
-        testScope.runTest {
-            val userId = 0
-            val specs by collectLastValue(underTest.autoAddedTiles(userId))
-
-            val value = "a,custom(b/c)"
-            storeForUser(value, userId)
-
-            assertThat(specs).isEqualTo(value.toSet())
-
-            val newValue = "a"
-            storeForUser(newValue, userId)
-
-            assertThat(specs).isEqualTo(newValue.toSet())
-        }
-
-    @Test
     fun tilesForCorrectUsers() =
         testScope.runTest {
-            val tilesFromUser0 by collectLastValue(underTest.autoAddedTiles(0))
-            val tilesFromUser1 by collectLastValue(underTest.autoAddedTiles(1))
-
             val user0Tiles = "a"
             val user1Tiles = "custom(b/c)"
             storeForUser(user0Tiles, 0)
             storeForUser(user1Tiles, 1)
+            val tilesFromUser0 by collectLastValue(underTest.autoAddedTiles(0))
+            val tilesFromUser1 by collectLastValue(underTest.autoAddedTiles(1))
+            runCurrent()
 
-            assertThat(tilesFromUser0).isEqualTo(user0Tiles.toSet())
-            assertThat(tilesFromUser1).isEqualTo(user1Tiles.toSet())
-        }
-
-    @Test
-    fun noInvalidTileSpecs() =
-        testScope.runTest {
-            val userId = 0
-            val tiles by collectLastValue(underTest.autoAddedTiles(userId))
-
-            val specs = "d,custom(bad)"
-            storeForUser(specs, userId)
-
-            assertThat(tiles).isEqualTo("d".toSet())
-        }
-
-    @Test
-    fun markAdded() =
-        testScope.runTest {
-            val userId = 0
-            val specs = mutableSetOf(TileSpec.create("a"))
-            underTest.markTileAdded(userId, TileSpec.create("a"))
-
-            assertThat(loadForUser(userId).toSet()).containsExactlyElementsIn(specs)
-
-            specs.add(TileSpec.create("b"))
-            underTest.markTileAdded(userId, TileSpec.create("b"))
-
-            assertThat(loadForUser(userId).toSet()).containsExactlyElementsIn(specs)
+            assertThat(tilesFromUser0).isEqualTo(user0Tiles.toTilesSet())
+            assertThat(tilesFromUser1).isEqualTo(user1Tiles.toTilesSet())
         }
 
     @Test
     fun markAdded_multipleUsers() =
         testScope.runTest {
+            val tilesFromUser0 by collectLastValue(underTest.autoAddedTiles(0))
+            val tilesFromUser1 by collectLastValue(underTest.autoAddedTiles(1))
+            runCurrent()
+
             underTest.markTileAdded(userId = 1, TileSpec.create("a"))
 
-            assertThat(loadForUser(0).toSet()).isEmpty()
-            assertThat(loadForUser(1).toSet())
-                .containsExactlyElementsIn(setOf(TileSpec.create("a")))
-        }
-
-    @Test
-    fun markAdded_Invalid_noop() =
-        testScope.runTest {
-            val userId = 0
-            underTest.markTileAdded(userId, TileSpec.Invalid)
-
-            assertThat(loadForUser(userId).toSet()).isEmpty()
-        }
-
-    @Test
-    fun unmarkAdded() =
-        testScope.runTest {
-            val userId = 0
-            val specs = "a,custom(b/c)"
-            storeForUser(specs, userId)
-
-            underTest.unmarkTileAdded(userId, TileSpec.create("a"))
-
-            assertThat(loadForUser(userId).toSet())
-                .containsExactlyElementsIn(setOf(TileSpec.create("custom(b/c)")))
+            assertThat(tilesFromUser0).isEmpty()
+            assertThat(tilesFromUser1).containsExactlyElementsIn(setOf(TileSpec.create("a")))
         }
 
     @Test
@@ -159,33 +104,23 @@
             val specs = "a,b"
             storeForUser(specs, 0)
             storeForUser(specs, 1)
+            val tilesFromUser0 by collectLastValue(underTest.autoAddedTiles(0))
+            val tilesFromUser1 by collectLastValue(underTest.autoAddedTiles(1))
+            runCurrent()
 
             underTest.unmarkTileAdded(1, TileSpec.create("a"))
 
-            assertThat(loadForUser(0).toSet()).isEqualTo(specs.toSet())
-            assertThat(loadForUser(1).toSet()).isEqualTo(setOf(TileSpec.create("b")))
+            assertThat(tilesFromUser0).isEqualTo(specs.toTilesSet())
+            assertThat(tilesFromUser1).isEqualTo(setOf(TileSpec.create("b")))
         }
 
     private fun storeForUser(specs: String, userId: Int) {
         secureSettings.putStringForUser(SETTING, specs, userId)
     }
 
-    private fun loadForUser(userId: Int): String {
-        return secureSettings.getStringForUser(SETTING, userId) ?: ""
-    }
-
     companion object {
         private const val SETTING = Settings.Secure.QS_AUTO_ADDED_TILES
-        private const val DELIMITER = ","
 
-        fun Set<TileSpec>.toSeparatedString() = joinToString(DELIMITER, transform = TileSpec::spec)
-
-        fun String.toSet(): Set<TileSpec> {
-            return if (isNullOrBlank()) {
-                emptySet()
-            } else {
-                split(DELIMITER).map(TileSpec::create).toSet()
-            }
-        }
+        private fun String.toTilesSet() = TilesSettingConverter.toTilesSet(this)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/QSSettingsRestoredBroadcastRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/QSSettingsRestoredBroadcastRepositoryTest.kt
new file mode 100644
index 0000000..dc09a33
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/QSSettingsRestoredBroadcastRepositoryTest.kt
@@ -0,0 +1,220 @@
+package com.android.systemui.qs.pipeline.data.repository
+
+import android.content.Intent
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.RoboPilotTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.FakeBroadcastDispatcher
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+@RoboPilotTest
+class QSSettingsRestoredBroadcastRepositoryTest : SysuiTestCase() {
+    private val dispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(dispatcher)
+
+    @Mock private lateinit var pipelineLogger: QSPipelineLogger
+
+    private lateinit var underTest: QSSettingsRestoredBroadcastRepository
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+
+        underTest =
+            QSSettingsRestoredBroadcastRepository(
+                fakeBroadcastDispatcher,
+                pipelineLogger,
+                testScope.backgroundScope,
+                dispatcher,
+            )
+    }
+
+    @Test
+    fun restoreDataAfterBothIntents_tilesRestoredFirst() =
+        testScope.runTest {
+            runCurrent()
+            val restoreData by collectLastValue(underTest.restoreData)
+            val user = 0
+
+            val tilesIntent =
+                createRestoreIntent(
+                    RestoreType.TILES,
+                    CURRENT_TILES,
+                    RESTORED_TILES,
+                )
+
+            val autoAddIntent =
+                createRestoreIntent(
+                    RestoreType.AUTOADD,
+                    CURRENT_AUTO_ADDED_TILES,
+                    RESTORED_AUTO_ADDED_TILES,
+                )
+
+            sendIntentForUser(tilesIntent, user)
+
+            // No restore data yet as we are missing one of the broadcasts
+            assertThat(restoreData).isNull()
+
+            // After the second event, we see the corresponding restore
+            sendIntentForUser(autoAddIntent, user)
+
+            with(restoreData!!) {
+                assertThat(restoredTiles).isEqualTo(RESTORED_TILES.toTilesList())
+                assertThat(restoredAutoAddedTiles).isEqualTo(RESTORED_AUTO_ADDED_TILES.toTilesSet())
+                assertThat(userId).isEqualTo(user)
+            }
+        }
+
+    @Test
+    fun restoreDataAfterBothIntents_autoAddRestoredFirst() =
+        testScope.runTest {
+            runCurrent()
+            val restoreData by collectLastValue(underTest.restoreData)
+            val user = 0
+
+            val tilesIntent =
+                createRestoreIntent(
+                    RestoreType.TILES,
+                    CURRENT_TILES,
+                    RESTORED_TILES,
+                )
+
+            val autoAddIntent =
+                createRestoreIntent(
+                    RestoreType.AUTOADD,
+                    CURRENT_AUTO_ADDED_TILES,
+                    RESTORED_AUTO_ADDED_TILES,
+                )
+
+            sendIntentForUser(autoAddIntent, user)
+
+            // No restore data yet as we are missing one of the broadcasts
+            assertThat(restoreData).isNull()
+
+            // After the second event, we see the corresponding restore
+            sendIntentForUser(tilesIntent, user)
+
+            with(restoreData!!) {
+                assertThat(restoredTiles).isEqualTo(RESTORED_TILES.toTilesList())
+                assertThat(restoredAutoAddedTiles).isEqualTo(RESTORED_AUTO_ADDED_TILES.toTilesSet())
+                assertThat(userId).isEqualTo(user)
+            }
+        }
+
+    @Test
+    fun interleavedBroadcastsFromDifferentUsers_onlysendDataForCorrectUser() =
+        testScope.runTest {
+            runCurrent()
+            val restoreData by collectLastValue(underTest.restoreData)
+
+            val user0 = 0
+            val user10 = 10
+
+            val currentTiles10 = "z,y,x"
+            val restoredTiles10 = "x"
+            val currentAutoAdded10 = "f"
+            val restoredAutoAdded10 = "f,g"
+
+            val tilesIntent0 =
+                createRestoreIntent(
+                    RestoreType.TILES,
+                    CURRENT_TILES,
+                    RESTORED_TILES,
+                )
+            val autoAddIntent0 =
+                createRestoreIntent(
+                    RestoreType.AUTOADD,
+                    CURRENT_AUTO_ADDED_TILES,
+                    RESTORED_AUTO_ADDED_TILES,
+                )
+            val tilesIntent10 =
+                createRestoreIntent(
+                    RestoreType.TILES,
+                    currentTiles10,
+                    restoredTiles10,
+                )
+            val autoAddIntent10 =
+                createRestoreIntent(
+                    RestoreType.AUTOADD,
+                    currentAutoAdded10,
+                    restoredAutoAdded10,
+                )
+
+            sendIntentForUser(tilesIntent0, user0)
+            sendIntentForUser(autoAddIntent10, user10)
+            assertThat(restoreData).isNull()
+
+            sendIntentForUser(tilesIntent10, user10)
+
+            with(restoreData!!) {
+                assertThat(restoredTiles).isEqualTo(restoredTiles10.toTilesList())
+                assertThat(restoredAutoAddedTiles).isEqualTo(restoredAutoAdded10.toTilesSet())
+                assertThat(userId).isEqualTo(user10)
+            }
+
+            sendIntentForUser(autoAddIntent0, user0)
+
+            with(restoreData!!) {
+                assertThat(restoredTiles).isEqualTo(RESTORED_TILES.toTilesList())
+                assertThat(restoredAutoAddedTiles).isEqualTo(RESTORED_AUTO_ADDED_TILES.toTilesSet())
+                assertThat(userId).isEqualTo(user0)
+            }
+        }
+
+    private fun sendIntentForUser(intent: Intent, userId: Int) {
+        fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+            context,
+            intent,
+            FakeBroadcastDispatcher.fakePendingResultForUser(userId)
+        )
+    }
+
+    companion object {
+        private const val CURRENT_TILES = "a,b,c,d"
+        private const val RESTORED_TILES = "b,a,c"
+        private const val CURRENT_AUTO_ADDED_TILES = "d"
+        private const val RESTORED_AUTO_ADDED_TILES = "e"
+
+        private fun createRestoreIntent(
+            type: RestoreType,
+            previousValue: String,
+            restoredValue: String,
+        ): Intent {
+            val setting =
+                when (type) {
+                    RestoreType.TILES -> Settings.Secure.QS_TILES
+                    RestoreType.AUTOADD -> Settings.Secure.QS_AUTO_ADDED_TILES
+                }
+            return Intent(Intent.ACTION_SETTING_RESTORED)
+                .putExtra(Intent.EXTRA_SETTING_NAME, setting)
+                .putExtra(Intent.EXTRA_SETTING_PREVIOUS_VALUE, previousValue)
+                .putExtra(Intent.EXTRA_SETTING_NEW_VALUE, restoredValue)
+        }
+
+        private fun String.toTilesList() = TilesSettingConverter.toTilesList(this)
+
+        private fun String.toTilesSet() = TilesSettingConverter.toTilesSet(this)
+
+        private enum class RestoreType {
+            TILES,
+            AUTOADD,
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt
index 1c28e4c..08adebb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt
@@ -23,14 +23,12 @@
 import com.android.systemui.RoboPilotTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.qs.QSHost
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
 import com.android.systemui.retail.data.repository.FakeRetailModeRepository
 import com.android.systemui.util.settings.FakeSettings
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
@@ -49,9 +47,28 @@
 
     private lateinit var secureSettings: FakeSettings
     private lateinit var retailModeRepository: FakeRetailModeRepository
+    private val defaultTilesRepository =
+        object : DefaultTilesRepository {
+            override val defaultTiles: List<TileSpec>
+                get() = DEFAULT_TILES.toTileSpecs()
+        }
 
     @Mock private lateinit var logger: QSPipelineLogger
 
+    private val userTileSpecRepositoryFactory =
+        object : UserTileSpecRepository.Factory {
+            override fun create(userId: Int): UserTileSpecRepository {
+                return UserTileSpecRepository(
+                    userId,
+                    defaultTilesRepository,
+                    secureSettings,
+                    logger,
+                    testScope.backgroundScope,
+                    testDispatcher,
+                )
+            }
+        }
+
     private val testDispatcher = StandardTestDispatcher()
     private val testScope = TestScope(testDispatcher)
 
@@ -66,293 +83,85 @@
         retailModeRepository.setRetailMode(false)
 
         with(context.orCreateTestableResources) {
-            addOverride(R.string.quick_settings_tiles_default, DEFAULT_TILES)
             addOverride(R.string.quick_settings_tiles_retail_mode, RETAIL_TILES)
         }
 
         underTest =
             TileSpecSettingsRepository(
-                secureSettings,
                 context.resources,
                 logger,
                 retailModeRepository,
-                testDispatcher,
+                userTileSpecRepositoryFactory
             )
     }
 
     @Test
-    fun emptySetting_usesDefaultValue() =
-        testScope.runTest {
-            val tiles by collectLastValue(underTest.tilesSpecs(0))
-            assertThat(tiles).isEqualTo(getDefaultTileSpecs())
-        }
-
-    @Test
-    fun changeInSettings_changesValue() =
-        testScope.runTest {
-            val tiles by collectLastValue(underTest.tilesSpecs(0))
-
-            storeTilesForUser("a", 0)
-            assertThat(tiles).isEqualTo(listOf(TileSpec.create("a")))
-
-            storeTilesForUser("a,custom(b/c)", 0)
-            assertThat(tiles)
-                .isEqualTo(listOf(TileSpec.create("a"), TileSpec.create("custom(b/c)")))
-        }
-
-    @Test
     fun tilesForCorrectUsers() =
         testScope.runTest {
-            val tilesFromUser0 by collectLastValue(underTest.tilesSpecs(0))
-            val tilesFromUser1 by collectLastValue(underTest.tilesSpecs(1))
-
             val user0Tiles = "a"
             val user1Tiles = "custom(b/c)"
             storeTilesForUser(user0Tiles, 0)
             storeTilesForUser(user1Tiles, 1)
 
+            val tilesFromUser0 by collectLastValue(underTest.tilesSpecs(0))
+            val tilesFromUser1 by collectLastValue(underTest.tilesSpecs(1))
+
             assertThat(tilesFromUser0).isEqualTo(user0Tiles.toTileSpecs())
             assertThat(tilesFromUser1).isEqualTo(user1Tiles.toTileSpecs())
         }
 
     @Test
-    fun invalidTilesAreNotPresent() =
-        testScope.runTest {
-            val tiles by collectLastValue(underTest.tilesSpecs(0))
-
-            val specs = "d,custom(bad)"
-            storeTilesForUser(specs, 0)
-
-            assertThat(tiles).isEqualTo(specs.toTileSpecs().filter { it != TileSpec.Invalid })
-        }
-
-    @Test
-    fun noValidTiles_defaultSet() =
-        testScope.runTest {
-            val tiles by collectLastValue(underTest.tilesSpecs(0))
-
-            storeTilesForUser("custom(bad),custom()", 0)
-
-            assertThat(tiles).isEqualTo(getDefaultTileSpecs())
-        }
-
-    @Test
-    fun addTileAtEnd() =
-        testScope.runTest {
-            val tiles by collectLastValue(underTest.tilesSpecs(0))
-
-            storeTilesForUser("a", 0)
-
-            underTest.addTile(userId = 0, TileSpec.create("b"))
-
-            val expected = "a,b"
-            assertThat(loadTilesForUser(0)).isEqualTo(expected)
-            assertThat(tiles).isEqualTo(expected.toTileSpecs())
-        }
-
-    @Test
-    fun addTileAtPosition() =
-        testScope.runTest {
-            val tiles by collectLastValue(underTest.tilesSpecs(0))
-
-            storeTilesForUser("a,custom(b/c)", 0)
-
-            underTest.addTile(userId = 0, TileSpec.create("d"), position = 1)
-
-            val expected = "a,d,custom(b/c)"
-            assertThat(loadTilesForUser(0)).isEqualTo(expected)
-            assertThat(tiles).isEqualTo(expected.toTileSpecs())
-        }
-
-    @Test
-    fun addInvalidTile_noop() =
-        testScope.runTest {
-            val tiles by collectLastValue(underTest.tilesSpecs(0))
-
-            val specs = "a,custom(b/c)"
-            storeTilesForUser(specs, 0)
-
-            underTest.addTile(userId = 0, TileSpec.Invalid)
-
-            assertThat(loadTilesForUser(0)).isEqualTo(specs)
-            assertThat(tiles).isEqualTo(specs.toTileSpecs())
-        }
-
-    @Test
-    fun addTileAtPosition_tooLarge_addedAtEnd() =
-        testScope.runTest {
-            val tiles by collectLastValue(underTest.tilesSpecs(0))
-
-            val specs = "a,custom(b/c)"
-            storeTilesForUser(specs, 0)
-
-            underTest.addTile(userId = 0, TileSpec.create("d"), position = 100)
-
-            val expected = "a,custom(b/c),d"
-            assertThat(loadTilesForUser(0)).isEqualTo(expected)
-            assertThat(tiles).isEqualTo(expected.toTileSpecs())
-        }
-
-    @Test
     fun addTileForOtherUser_addedInThatUser() =
         testScope.runTest {
-            val tilesUser0 by collectLastValue(underTest.tilesSpecs(0))
-            val tilesUser1 by collectLastValue(underTest.tilesSpecs(1))
-
             storeTilesForUser("a", 0)
             storeTilesForUser("b", 1)
+            val tilesUser0 by collectLastValue(underTest.tilesSpecs(0))
+            val tilesUser1 by collectLastValue(underTest.tilesSpecs(1))
+            runCurrent()
 
             underTest.addTile(userId = 1, TileSpec.create("c"))
 
-            assertThat(loadTilesForUser(0)).isEqualTo("a")
             assertThat(tilesUser0).isEqualTo("a".toTileSpecs())
-            assertThat(loadTilesForUser(1)).isEqualTo("b,c")
+            assertThat(loadTilesForUser(0)).isEqualTo("a")
             assertThat(tilesUser1).isEqualTo("b,c".toTileSpecs())
-        }
-
-    @Test
-    fun removeTiles() =
-        testScope.runTest {
-            val tiles by collectLastValue(underTest.tilesSpecs(0))
-
-            storeTilesForUser("a,b", 0)
-
-            underTest.removeTiles(userId = 0, listOf(TileSpec.create("a")))
-
-            assertThat(loadTilesForUser(0)).isEqualTo("b")
-            assertThat(tiles).isEqualTo("b".toTileSpecs())
-        }
-
-    @Test
-    fun removeTilesNotThere_noop() =
-        testScope.runTest {
-            val tiles by collectLastValue(underTest.tilesSpecs(0))
-
-            val specs = "a,b"
-            storeTilesForUser(specs, 0)
-
-            underTest.removeTiles(userId = 0, listOf(TileSpec.create("c")))
-
-            assertThat(loadTilesForUser(0)).isEqualTo(specs)
-            assertThat(tiles).isEqualTo(specs.toTileSpecs())
-        }
-
-    @Test
-    fun removeInvalidTile_noop() =
-        testScope.runTest {
-            val tiles by collectLastValue(underTest.tilesSpecs(0))
-
-            val specs = "a,b"
-            storeTilesForUser(specs, 0)
-
-            underTest.removeTiles(userId = 0, listOf(TileSpec.Invalid))
-
-            assertThat(loadTilesForUser(0)).isEqualTo(specs)
-            assertThat(tiles).isEqualTo(specs.toTileSpecs())
+            assertThat(loadTilesForUser(1)).isEqualTo("b,c")
         }
 
     @Test
     fun removeTileFromSecondaryUser_removedOnlyInCorrectUser() =
         testScope.runTest {
-            val user0Tiles by collectLastValue(underTest.tilesSpecs(0))
-            val user1Tiles by collectLastValue(underTest.tilesSpecs(1))
-
             val specs = "a,b"
             storeTilesForUser(specs, 0)
             storeTilesForUser(specs, 1)
+            val user0Tiles by collectLastValue(underTest.tilesSpecs(0))
+            val user1Tiles by collectLastValue(underTest.tilesSpecs(1))
+            runCurrent()
 
             underTest.removeTiles(userId = 1, listOf(TileSpec.create("a")))
 
-            assertThat(loadTilesForUser(0)).isEqualTo(specs)
             assertThat(user0Tiles).isEqualTo(specs.toTileSpecs())
-            assertThat(loadTilesForUser(1)).isEqualTo("b")
+            assertThat(loadTilesForUser(0)).isEqualTo(specs)
             assertThat(user1Tiles).isEqualTo("b".toTileSpecs())
-        }
-
-    @Test
-    fun removeMultipleTiles() =
-        testScope.runTest {
-            val tiles by collectLastValue(underTest.tilesSpecs(0))
-
-            storeTilesForUser("a,b,c,d", 0)
-
-            underTest.removeTiles(userId = 0, listOf(TileSpec.create("a"), TileSpec.create("c")))
-
-            assertThat(loadTilesForUser(0)).isEqualTo("b,d")
-            assertThat(tiles).isEqualTo("b,d".toTileSpecs())
-        }
-
-    @Test
-    fun changeTiles() =
-        testScope.runTest {
-            val tiles by collectLastValue(underTest.tilesSpecs(0))
-
-            val specs = "a,custom(b/c)"
-
-            underTest.setTiles(userId = 0, specs.toTileSpecs())
-
-            assertThat(loadTilesForUser(0)).isEqualTo(specs)
-            assertThat(tiles).isEqualTo(specs.toTileSpecs())
-        }
-
-    @Test
-    fun changeTiles_ignoresInvalid() =
-        testScope.runTest {
-            val tiles by collectLastValue(underTest.tilesSpecs(0))
-
-            val specs = "a,custom(b/c)"
-
-            underTest.setTiles(userId = 0, listOf(TileSpec.Invalid) + specs.toTileSpecs())
-
-            assertThat(loadTilesForUser(0)).isEqualTo(specs)
-            assertThat(tiles).isEqualTo(specs.toTileSpecs())
-        }
-
-    @Test
-    fun changeTiles_empty_noChanges() =
-        testScope.runTest {
-            val tiles by collectLastValue(underTest.tilesSpecs(0))
-
-            underTest.setTiles(userId = 0, emptyList())
-
-            assertThat(loadTilesForUser(0)).isNull()
-            assertThat(tiles).isEqualTo(getDefaultTileSpecs())
+            assertThat(loadTilesForUser(1)).isEqualTo("b")
         }
 
     @Test
     fun changeTiles_forCorrectUser() =
         testScope.runTest {
-            val user0Tiles by collectLastValue(underTest.tilesSpecs(0))
-            val user1Tiles by collectLastValue(underTest.tilesSpecs(1))
-
             val specs = "a"
             storeTilesForUser(specs, 0)
             storeTilesForUser(specs, 1)
+            val user0Tiles by collectLastValue(underTest.tilesSpecs(0))
+            val user1Tiles by collectLastValue(underTest.tilesSpecs(1))
+            runCurrent()
 
             underTest.setTiles(userId = 1, "b".toTileSpecs())
 
-            assertThat(loadTilesForUser(0)).isEqualTo("a")
             assertThat(user0Tiles).isEqualTo(specs.toTileSpecs())
+            assertThat(loadTilesForUser(0)).isEqualTo("a")
 
-            assertThat(loadTilesForUser(1)).isEqualTo("b")
             assertThat(user1Tiles).isEqualTo("b".toTileSpecs())
-        }
-
-    @Test
-    fun multipleConcurrentRemovals_bothRemoved() =
-        testScope.runTest {
-            val tiles by collectLastValue(underTest.tilesSpecs(0))
-
-            val specs = "a,b,c"
-            storeTilesForUser(specs, 0)
-
-            coroutineScope {
-                underTest.removeTiles(userId = 0, listOf(TileSpec.create("c")))
-                underTest.removeTiles(userId = 0, listOf(TileSpec.create("a")))
-            }
-
-            assertThat(loadTilesForUser(0)).isEqualTo("b")
-            assertThat(tiles).isEqualTo("b".toTileSpecs())
+            assertThat(loadTilesForUser(1)).isEqualTo("b")
         }
 
     @Test
@@ -361,6 +170,7 @@
             retailModeRepository.setRetailMode(true)
 
             val tiles by collectLastValue(underTest.tilesSpecs(0))
+            runCurrent()
 
             assertThat(tiles).isEqualTo(RETAIL_TILES.toTileSpecs())
         }
@@ -369,25 +179,13 @@
     fun retailMode_cannotModifyTiles() =
         testScope.runTest {
             retailModeRepository.setRetailMode(true)
-
-            underTest.setTiles(0, DEFAULT_TILES.toTileSpecs())
-
-            assertThat(loadTilesForUser(0)).isNull()
-        }
-
-    @Test
-    fun emptyTilesReplacedByDefaultInSettings() =
-        testScope.runTest {
             val tiles by collectLastValue(underTest.tilesSpecs(0))
             runCurrent()
 
-            assertThat(loadTilesForUser(0))
-                .isEqualTo(getDefaultTileSpecs().map { it.spec }.joinToString(","))
-        }
+            underTest.setTiles(0, listOf(TileSpec.create("a")))
 
-    private fun getDefaultTileSpecs(): List<TileSpec> {
-        return QSHost.getDefaultSpecs(context.resources).map(TileSpec::create)
-    }
+            assertThat(loadTilesForUser(0)).isEqualTo(DEFAULT_TILES)
+        }
 
     private fun TestScope.storeTilesForUser(specs: String, forUser: Int) {
         secureSettings.putStringForUser(SETTING, specs, forUser)
@@ -403,8 +201,6 @@
         private const val RETAIL_TILES = "d"
         private const val SETTING = Settings.Secure.QS_TILES
 
-        private fun String.toTileSpecs(): List<TileSpec> {
-            return split(",").map(TileSpec::create)
-        }
+        private fun String.toTileSpecs() = TilesSettingConverter.toTilesList(this)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TilesSettingConverterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TilesSettingConverterTest.kt
new file mode 100644
index 0000000..2087623
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TilesSettingConverterTest.kt
@@ -0,0 +1,100 @@
+package com.android.systemui.qs.pipeline.data.repository
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.RoboPilotTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RoboPilotTest
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TilesSettingConverterTest : SysuiTestCase() {
+
+    @Test
+    fun toTilesList_correctContentAndOrdering() {
+        val specString =
+            listOf(
+                    "c",
+                    "b",
+                    "custom(x/y)",
+                    "d",
+                )
+                .joinToString(DELIMITER)
+
+        val expected =
+            listOf(
+                TileSpec.create("c"),
+                TileSpec.create("b"),
+                TileSpec.create("custom(x/y)"),
+                TileSpec.create("d"),
+            )
+
+        assertThat(TilesSettingConverter.toTilesList(specString)).isEqualTo(expected)
+    }
+
+    @Test
+    fun toTilesList_removesInvalid() {
+        val specString =
+            listOf(
+                    "a",
+                    "",
+                    "b",
+                )
+                .joinToString(DELIMITER)
+        assertThat(TileSpec.create("")).isEqualTo(TileSpec.Invalid)
+        val expected =
+            listOf(
+                TileSpec.create("a"),
+                TileSpec.create("b"),
+            )
+        assertThat(TilesSettingConverter.toTilesList(specString)).isEqualTo(expected)
+    }
+
+    @Test
+    fun toTilesSet_correctContent() {
+        val specString =
+            listOf(
+                    "c",
+                    "b",
+                    "custom(x/y)",
+                    "d",
+                )
+                .joinToString(DELIMITER)
+
+        val expected =
+            setOf(
+                TileSpec.create("c"),
+                TileSpec.create("b"),
+                TileSpec.create("custom(x/y)"),
+                TileSpec.create("d"),
+            )
+
+        assertThat(TilesSettingConverter.toTilesSet(specString)).isEqualTo(expected)
+    }
+
+    @Test
+    fun toTilesSet_removesInvalid() {
+        val specString =
+            listOf(
+                    "a",
+                    "",
+                    "b",
+                )
+                .joinToString(DELIMITER)
+        assertThat(TileSpec.create("")).isEqualTo(TileSpec.Invalid)
+        val expected =
+            setOf(
+                TileSpec.create("a"),
+                TileSpec.create("b"),
+            )
+        assertThat(TilesSettingConverter.toTilesSet(specString)).isEqualTo(expected)
+    }
+
+    companion object {
+        private const val DELIMITER = ","
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/UserAutoAddRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/UserAutoAddRepositoryTest.kt
new file mode 100644
index 0000000..81fd72b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/UserAutoAddRepositoryTest.kt
@@ -0,0 +1,160 @@
+package com.android.systemui.qs.pipeline.data.repository
+
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.RoboPilotTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.qs.pipeline.data.model.RestoreData
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RoboPilotTest
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class UserAutoAddRepositoryTest : SysuiTestCase() {
+    private val secureSettings = FakeSettings()
+
+    @Mock private lateinit var logger: QSPipelineLogger
+
+    private val testDispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
+
+    private lateinit var underTest: UserAutoAddRepository
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        underTest =
+            UserAutoAddRepository(
+                USER,
+                secureSettings,
+                logger,
+                testScope.backgroundScope,
+                testDispatcher,
+            )
+    }
+
+    @Test
+    fun nonExistentSetting_emptySet() =
+        testScope.runTest {
+            val specs by collectLastValue(underTest.autoAdded())
+
+            assertThat(specs).isEmpty()
+        }
+
+    @Test
+    fun settingsChange_noChanges() =
+        testScope.runTest {
+            val value = "a,custom(b/c)"
+            store(value)
+            val specs by collectLastValue(underTest.autoAdded())
+            runCurrent()
+
+            assertThat(specs).isEqualTo(value.toTilesSet())
+
+            val newValue = "a"
+            store(newValue)
+
+            assertThat(specs).isEqualTo(value.toTilesSet())
+        }
+
+    @Test
+    fun noInvalidTileSpecs() =
+        testScope.runTest {
+            val specs = "d,custom(bad)"
+            store(specs)
+            val tiles by collectLastValue(underTest.autoAdded())
+            runCurrent()
+
+            assertThat(tiles).isEqualTo("d".toTilesSet())
+        }
+
+    @Test
+    fun markAdded() =
+        testScope.runTest {
+            val specs = mutableSetOf(TileSpec.create("a"))
+            val autoAdded by collectLastValue(underTest.autoAdded())
+            runCurrent()
+
+            underTest.markTileAdded(TileSpec.create("a"))
+
+            assertThat(autoAdded).containsExactlyElementsIn(specs)
+
+            specs.add(TileSpec.create("b"))
+            underTest.markTileAdded(TileSpec.create("b"))
+
+            assertThat(autoAdded).containsExactlyElementsIn(specs)
+        }
+
+    @Test
+    fun markAdded_Invalid_noop() =
+        testScope.runTest {
+            val autoAdded by collectLastValue(underTest.autoAdded())
+            runCurrent()
+
+            underTest.markTileAdded(TileSpec.Invalid)
+
+            Truth.assertThat(autoAdded).isEmpty()
+        }
+
+    @Test
+    fun unmarkAdded() =
+        testScope.runTest {
+            val specs = "a,custom(b/c)"
+            store(specs)
+            val autoAdded by collectLastValue(underTest.autoAdded())
+            runCurrent()
+
+            underTest.unmarkTileAdded(TileSpec.create("a"))
+
+            assertThat(autoAdded).containsExactlyElementsIn(setOf(TileSpec.create("custom(b/c)")))
+        }
+
+    @Test
+    fun restore_addsRestoredTiles() =
+        testScope.runTest {
+            val specs = "a,b"
+            val restored = "b,c"
+            store(specs)
+            val autoAdded by collectLastValue(underTest.autoAdded())
+            runCurrent()
+
+            val restoreData =
+                RestoreData(
+                    emptyList(),
+                    restored.toTilesSet(),
+                    USER,
+                )
+            underTest.reconcileRestore(restoreData)
+
+            assertThat(autoAdded).containsExactlyElementsIn("a,b,c".toTilesSet())
+        }
+
+    private fun store(specs: String) {
+        secureSettings.putStringForUser(SETTING, specs, USER)
+    }
+
+    companion object {
+        private const val USER = 10
+        private const val SETTING = Settings.Secure.QS_AUTO_ADDED_TILES
+
+        private fun String.toTilesSet() = TilesSettingConverter.toTilesSet(this)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt
new file mode 100644
index 0000000..389580c1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt
@@ -0,0 +1,351 @@
+package com.android.systemui.qs.pipeline.data.repository
+
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.RoboPilotTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.qs.pipeline.data.model.RestoreData
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@RoboPilotTest
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+class UserTileSpecRepositoryTest : SysuiTestCase() {
+    private val secureSettings = FakeSettings()
+    private val defaultTilesRepository =
+        object : DefaultTilesRepository {
+            override val defaultTiles: List<TileSpec>
+                get() = DEFAULT_TILES.toTileSpecs()
+        }
+
+    @Mock private lateinit var logger: QSPipelineLogger
+
+    private val testDispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
+
+    private lateinit var underTest: UserTileSpecRepository
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+
+        underTest =
+            UserTileSpecRepository(
+                USER,
+                defaultTilesRepository,
+                secureSettings,
+                logger,
+                testScope.backgroundScope,
+                testDispatcher,
+            )
+    }
+
+    @Test
+    fun emptySetting_usesDefaultValue() =
+        testScope.runTest {
+            val tiles by collectLastValue(underTest.tiles())
+            assertThat(tiles).isEqualTo(getDefaultTileSpecs())
+        }
+
+    @Test
+    fun changeInSettings_valueDoesntChange() =
+        testScope.runTest {
+            storeTiles("a")
+            val tiles by collectLastValue(underTest.tiles())
+
+            assertThat(tiles).isEqualTo(listOf(TileSpec.create("a")))
+
+            storeTiles("a,custom(b/c)")
+            assertThat(tiles).isEqualTo(listOf(TileSpec.create("a")))
+        }
+
+    @Test
+    fun changeInSettings_settingIsRestored() =
+        testScope.runTest {
+            storeTiles("a")
+            val tiles by collectLastValue(underTest.tiles())
+            runCurrent()
+
+            storeTiles("a,custom(b/c)")
+            assertThat(loadTiles()).isEqualTo("a")
+        }
+
+    @Test
+    fun invalidTilesAreNotPresent() =
+        testScope.runTest {
+            val specs = "d,custom(bad)"
+            storeTiles(specs)
+
+            val tiles by collectLastValue(underTest.tiles())
+
+            assertThat(tiles).isEqualTo(specs.toTileSpecs().filter { it != TileSpec.Invalid })
+        }
+
+    @Test
+    fun noValidTiles_defaultSet() =
+        testScope.runTest {
+            storeTiles("custom(bad),custom()")
+
+            val tiles by collectLastValue(underTest.tiles())
+
+            assertThat(tiles).isEqualTo(getDefaultTileSpecs())
+        }
+
+    /*
+     * Following tests are for the possible actions that can be performed to the list of tiles.
+     * In general, the tests follow this scheme:
+     *
+     * 1. Set starting tiles in Settings
+     * 2. Start collection of flows
+     * 3. Call `runCurrent` so all collectors are started (side effects)
+     * 4. Perform operation
+     * 5. Check that the flow contains the right value
+     * 6. Check that settings contains the right value.
+     */
+
+    @Test
+    fun addTileAtEnd() =
+        testScope.runTest {
+            storeTiles("a")
+            val tiles by collectLastValue(underTest.tiles())
+            runCurrent()
+
+            underTest.addTile(TileSpec.create("b"))
+
+            val expected = "a,b"
+            assertThat(tiles).isEqualTo(expected.toTileSpecs())
+            assertThat(loadTiles()).isEqualTo(expected)
+        }
+
+    @Test
+    fun addTileAtPosition() =
+        testScope.runTest {
+            storeTiles("a,custom(b/c)")
+            val tiles by collectLastValue(underTest.tiles())
+            runCurrent()
+
+            underTest.addTile(TileSpec.create("d"), position = 1)
+
+            val expected = "a,d,custom(b/c)"
+            assertThat(tiles).isEqualTo(expected.toTileSpecs())
+            assertThat(loadTiles()).isEqualTo(expected)
+        }
+
+    @Test
+    fun addInvalidTile_noop() =
+        testScope.runTest {
+            val specs = "a,custom(b/c)"
+            storeTiles(specs)
+            val tiles by collectLastValue(underTest.tiles())
+            runCurrent()
+
+            underTest.addTile(TileSpec.Invalid)
+
+            assertThat(tiles).isEqualTo(specs.toTileSpecs())
+            assertThat(loadTiles()).isEqualTo(specs)
+        }
+
+    @Test
+    fun addTileAtPosition_tooLarge_addedAtEnd() =
+        testScope.runTest {
+            val specs = "a,custom(b/c)"
+            storeTiles(specs)
+            val tiles by collectLastValue(underTest.tiles())
+            runCurrent()
+
+            underTest.addTile(TileSpec.create("d"), position = 100)
+
+            val expected = "a,custom(b/c),d"
+            assertThat(tiles).isEqualTo(expected.toTileSpecs())
+            assertThat(loadTiles()).isEqualTo(expected)
+        }
+
+    @Test
+    fun removeTiles() =
+        testScope.runTest {
+            storeTiles("a,b")
+            val tiles by collectLastValue(underTest.tiles())
+            runCurrent()
+
+            underTest.removeTiles(listOf(TileSpec.create("a")))
+
+            assertThat(tiles).isEqualTo("b".toTileSpecs())
+            assertThat(loadTiles()).isEqualTo("b")
+        }
+
+    @Test
+    fun removeTilesNotThere_noop() =
+        testScope.runTest {
+            val specs = "a,b"
+            storeTiles(specs)
+            val tiles by collectLastValue(underTest.tiles())
+            runCurrent()
+
+            underTest.removeTiles(listOf(TileSpec.create("c")))
+
+            assertThat(tiles).isEqualTo(specs.toTileSpecs())
+            assertThat(loadTiles()).isEqualTo(specs)
+        }
+
+    @Test
+    fun removeInvalidTile_noop() =
+        testScope.runTest {
+            val specs = "a,b"
+            storeTiles(specs)
+            val tiles by collectLastValue(underTest.tiles())
+            runCurrent()
+
+            underTest.removeTiles(listOf(TileSpec.Invalid))
+
+            assertThat(tiles).isEqualTo(specs.toTileSpecs())
+            assertThat(loadTiles()).isEqualTo(specs)
+        }
+
+    @Test
+    fun removeMultipleTiles() =
+        testScope.runTest {
+            storeTiles("a,b,c,d")
+            val tiles by collectLastValue(underTest.tiles())
+            runCurrent()
+
+            underTest.removeTiles(listOf(TileSpec.create("a"), TileSpec.create("c")))
+
+            assertThat(tiles).isEqualTo("b,d".toTileSpecs())
+            assertThat(loadTiles()).isEqualTo("b,d")
+        }
+
+    @Test
+    fun changeTiles() =
+        testScope.runTest {
+            val specs = "a,custom(b/c)"
+            val tiles by collectLastValue(underTest.tiles())
+            runCurrent()
+
+            underTest.setTiles(specs.toTileSpecs())
+
+            assertThat(tiles).isEqualTo(specs.toTileSpecs())
+            assertThat(loadTiles()).isEqualTo(specs)
+        }
+
+    @Test
+    fun changeTiles_ignoresInvalid() =
+        testScope.runTest {
+            val specs = "a,custom(b/c)"
+            val tiles by collectLastValue(underTest.tiles())
+            runCurrent()
+
+            underTest.setTiles(listOf(TileSpec.Invalid) + specs.toTileSpecs())
+
+            assertThat(tiles).isEqualTo(specs.toTileSpecs())
+            assertThat(loadTiles()).isEqualTo(specs)
+        }
+
+    @Test
+    fun changeTiles_empty_noChanges() =
+        testScope.runTest {
+            val specs = "a,b,c,d"
+            storeTiles(specs)
+            val tiles by collectLastValue(underTest.tiles())
+            runCurrent()
+
+            underTest.setTiles(emptyList())
+
+            assertThat(tiles).isEqualTo(specs.toTileSpecs())
+            assertThat(loadTiles()).isEqualTo(specs)
+        }
+
+    @Test
+    fun multipleConcurrentRemovals_bothRemoved() =
+        testScope.runTest {
+            val specs = "a,b,c"
+            storeTiles(specs)
+            val tiles by collectLastValue(underTest.tiles())
+            runCurrent()
+
+            coroutineScope {
+                underTest.removeTiles(listOf(TileSpec.create("c")))
+                underTest.removeTiles(listOf(TileSpec.create("a")))
+            }
+
+            assertThat(tiles).isEqualTo("b".toTileSpecs())
+            assertThat(loadTiles()).isEqualTo("b")
+        }
+
+    @Test
+    fun emptyTilesReplacedByDefaultInSettings() =
+        testScope.runTest {
+            val tiles by collectLastValue(underTest.tiles())
+            runCurrent()
+
+            assertThat(loadTiles())
+                .isEqualTo(getDefaultTileSpecs().map { it.spec }.joinToString(","))
+        }
+
+    @Test
+    fun restoreDataIsProperlyReconciled() =
+        testScope.runTest {
+            // Tile b was just auto-added, so we should re-add it in position 1
+            // Tile e was auto-added before, but the user had removed it (not in the restored set).
+            // It should not be re-added
+            val specsBeforeRestore = "a,b,c,d,e"
+            val restoredSpecs = "a,c,d,f"
+            val autoAddedBeforeRestore = "b,d"
+            val restoredAutoAdded = "d,e"
+
+            storeTiles(specsBeforeRestore)
+            val tiles by collectLastValue(underTest.tiles())
+            runCurrent()
+
+            val restoreData =
+                RestoreData(
+                    restoredSpecs.toTileSpecs(),
+                    restoredAutoAdded.toTilesSet(),
+                    USER,
+                )
+            underTest.reconcileRestore(restoreData, autoAddedBeforeRestore.toTilesSet())
+            runCurrent()
+
+            val expected = "a,b,c,d,f"
+            assertThat(tiles).isEqualTo(expected.toTileSpecs())
+            assertThat(loadTiles()).isEqualTo(expected)
+        }
+
+    private fun getDefaultTileSpecs(): List<TileSpec> {
+        return defaultTilesRepository.defaultTiles
+    }
+
+    private fun TestScope.storeTiles(specs: String) {
+        secureSettings.putStringForUser(SETTING, specs, USER)
+        runCurrent()
+    }
+
+    private fun loadTiles(): String? {
+        return secureSettings.getStringForUser(SETTING, USER)
+    }
+
+    companion object {
+        private const val USER = 10
+        private const val DEFAULT_TILES = "a,b,c"
+        private const val SETTING = Settings.Secure.QS_TILES
+
+        private fun String.toTileSpecs() = TilesSettingConverter.toTilesList(this)
+        private fun String.toTilesSet() = TilesSettingConverter.toTilesSet(this)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/RestoreReconciliationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/RestoreReconciliationInteractorTest.kt
new file mode 100644
index 0000000..5630b9d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/domain/interactor/RestoreReconciliationInteractorTest.kt
@@ -0,0 +1,94 @@
+package com.android.systemui.qs.pipeline.domain.interactor
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.RoboPilotTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.qs.pipeline.data.model.RestoreData
+import com.android.systemui.qs.pipeline.data.repository.FakeAutoAddRepository
+import com.android.systemui.qs.pipeline.data.repository.FakeQSSettingsRestoredRepository
+import com.android.systemui.qs.pipeline.data.repository.FakeTileSpecRepository
+import com.android.systemui.qs.pipeline.data.repository.TilesSettingConverter
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+
+@RoboPilotTest
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class RestoreReconciliationInteractorTest : SysuiTestCase() {
+
+    private val tileSpecRepository = FakeTileSpecRepository()
+    private val autoAddRepository = FakeAutoAddRepository()
+
+    private val qsSettingsRestoredRepository = FakeQSSettingsRestoredRepository()
+
+    private lateinit var underTest: RestoreReconciliationInteractor
+
+    private val testDispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        underTest =
+            RestoreReconciliationInteractor(
+                tileSpecRepository,
+                autoAddRepository,
+                qsSettingsRestoredRepository,
+                testScope.backgroundScope,
+                testDispatcher
+            )
+        underTest.start()
+    }
+
+    @Test
+    fun reconciliationInCorrectOrder_hascurrentAutoAdded() =
+        testScope.runTest {
+            val user = 10
+            val tiles by collectLastValue(tileSpecRepository.tilesSpecs(user))
+            val autoAdd by collectLastValue(autoAddRepository.autoAddedTiles(user))
+
+            // Tile b was just auto-added, so we should re-add it in position 1
+            // Tile e was auto-added before, but the user had removed it (not in the restored set).
+            // It should not be re-added
+            val specsBeforeRestore = "a,b,c,d,e"
+            val restoredSpecs = "a,c,d,f"
+            val autoAddedBeforeRestore = "b,d"
+            val restoredAutoAdded = "d,e"
+
+            val restoreData =
+                RestoreData(
+                    restoredSpecs.toTilesList(),
+                    restoredAutoAdded.toTilesSet(),
+                    user,
+                )
+
+            autoAddedBeforeRestore.toTilesSet().forEach {
+                autoAddRepository.markTileAdded(user, it)
+            }
+            tileSpecRepository.setTiles(user, specsBeforeRestore.toTilesList())
+
+            qsSettingsRestoredRepository.onDataRestored(restoreData)
+            runCurrent()
+
+            val expectedTiles = "a,b,c,d,f"
+            assertThat(tiles).isEqualTo(expectedTiles.toTilesList())
+
+            val expectedAutoAdd = "b,d,e"
+            assertThat(autoAdd).isEqualTo(expectedAutoAdd.toTilesSet())
+        }
+
+    companion object {
+        private fun String.toTilesList() = TilesSettingConverter.toTilesList(this)
+        private fun String.toTilesSet() = TilesSettingConverter.toTilesSet(this)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotSoundControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotSoundControllerTest.kt
new file mode 100644
index 0000000..091531e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotSoundControllerTest.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.screenshot
+
+import android.media.MediaPlayer
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import java.lang.IllegalStateException
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@SmallTest
+class ScreenshotSoundControllerTest : SysuiTestCase() {
+
+    private val soundProvider = mock<ScreenshotSoundProvider>()
+    private val mediaPlayer = mock<MediaPlayer>()
+    private val bgDispatcher = UnconfinedTestDispatcher()
+    private val scope = TestScope(bgDispatcher)
+    @Before
+    fun setup() {
+        whenever(soundProvider.getScreenshotSound()).thenReturn(mediaPlayer)
+    }
+
+    @Test
+    fun init_soundLoading() {
+        createController()
+        bgDispatcher.scheduler.runCurrent()
+
+        verify(soundProvider).getScreenshotSound()
+    }
+
+    @Test
+    fun init_soundLoadingException_playAndReleaseDoNotThrow() = runTest {
+        whenever(soundProvider.getScreenshotSound()).thenThrow(IllegalStateException())
+
+        val controller = createController()
+
+        controller.playCameraSound().await()
+        controller.releaseScreenshotSound().await()
+
+        verify(mediaPlayer, never()).start()
+        verify(mediaPlayer, never()).release()
+    }
+
+    @Test
+    fun playCameraSound_soundLoadingSuccessful_mediaPlayerPlays() = runTest {
+        val controller = createController()
+
+        controller.playCameraSound().await()
+
+        verify(mediaPlayer).start()
+    }
+
+    @Test
+    fun playCameraSound_soundLoadingSuccessful_mediaPlayerReleases() = runTest {
+        val controller = createController()
+
+        controller.releaseScreenshotSound().await()
+
+        verify(mediaPlayer).release()
+    }
+
+    private fun createController() =
+        ScreenshotSoundControllerImpl(soundProvider, scope, bgDispatcher)
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 31c8a3d7..ed731dd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -118,7 +118,7 @@
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.qs.QS;
-import com.android.systemui.qs.QSFragment;
+import com.android.systemui.qs.QSFragmentLegacy;
 import com.android.systemui.screenrecord.RecordingController;
 import com.android.systemui.shade.data.repository.FakeShadeRepository;
 import com.android.systemui.shade.data.repository.ShadeRepository;
@@ -291,7 +291,7 @@
     @Mock protected UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
     @Mock protected ShadeTransitionController mShadeTransitionController;
     @Mock protected QS mQs;
-    @Mock protected QSFragment mQSFragment;
+    @Mock protected QSFragmentLegacy mQSFragment;
     @Mock protected ViewGroup mQsHeader;
     @Mock protected ViewParent mViewParent;
     @Mock protected ViewTreeObserver mViewTreeObserver;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQuickSettingsContainerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQuickSettingsContainerTest.kt
index f7d2497..0c3af03 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQuickSettingsContainerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQuickSettingsContainerTest.kt
@@ -22,9 +22,9 @@
 import android.widget.FrameLayout
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.qs.QSFragment
+import com.android.systemui.qs.QSFragmentLegacy
+import com.android.systemui.res.R
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -40,7 +40,7 @@
     @Mock private lateinit var qsFrame: View
     @Mock private lateinit var stackScroller: View
     @Mock private lateinit var keyguardStatusBar: View
-    @Mock private lateinit var qsFragment: QSFragment
+    @Mock private lateinit var qsFragment: QSFragmentLegacy
 
     private lateinit var qsView: ViewGroup
     private lateinit var qsContainer: View
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
index fb0d4db..8138b32 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
@@ -34,7 +34,6 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.keyguard.KeyguardStatusView;
 import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.systemui.res.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository;
 import com.android.systemui.dump.DumpManager;
@@ -46,7 +45,8 @@
 import com.android.systemui.media.controls.ui.MediaHierarchyManager;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.qs.QS;
-import com.android.systemui.qs.QSFragment;
+import com.android.systemui.qs.QSFragmentLegacy;
+import com.android.systemui.res.R;
 import com.android.systemui.scene.SceneTestUtils;
 import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags;
 import com.android.systemui.screenrecord.RecordingController;
@@ -75,18 +75,17 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository;
 import com.android.systemui.statusbar.policy.CastController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
-import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController;
 import com.android.systemui.user.domain.interactor.UserInteractor;
 import com.android.systemui.util.kotlin.JavaAdapter;
 
-import dagger.Lazy;
-
 import org.junit.After;
 import org.junit.Before;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import dagger.Lazy;
 import kotlinx.coroutines.test.TestScope;
 
 public class QuickSettingsControllerBaseTest extends SysuiTestCase {
@@ -109,7 +108,7 @@
     @Mock protected KeyguardBottomAreaView mQsFrame;
     @Mock protected KeyguardStatusBarView mKeyguardStatusBar;
     @Mock protected QS mQs;
-    @Mock protected QSFragment mQSFragment;
+    @Mock protected QSFragmentLegacy mQSFragment;
     @Mock protected Lazy<NotificationPanelViewController> mPanelViewControllerLazy;
     @Mock protected NotificationPanelViewController mNotificationPanelViewController;
     @Mock protected NotificationPanelView mPanelView;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
index 99e4030..b54fbd3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
@@ -90,6 +90,8 @@
     private val _defaultMobileIconGroup = MutableStateFlow(DEFAULT_ICON)
     override val defaultMobileIconGroup = _defaultMobileIconGroup
 
+    override val isAnySimSecure = MutableStateFlow(false)
+
     fun setSubscriptions(subs: List<SubscriptionModel>) {
         _subscriptions.value = subs
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
index d005972..4d4f33b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
@@ -135,6 +135,7 @@
                 FakeAirplaneModeRepository(),
                 wifiRepository,
                 mock(),
+                mock(),
             )
 
         demoRepo =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
index 6f9764a..9148c75 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
@@ -37,6 +37,8 @@
 import android.testing.TestableLooper
 import androidx.test.filters.SmallTest
 import com.android.internal.telephony.PhoneConstants
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.settingslib.R
 import com.android.settingslib.mobile.MobileMappings
 import com.android.systemui.SysuiTestCase
@@ -104,6 +106,7 @@
     @Mock private lateinit var logger: MobileInputLogger
     @Mock private lateinit var summaryLogger: TableLogBuffer
     @Mock private lateinit var logBufferFactory: TableLogBufferFactory
+    @Mock private lateinit var updateMonitor: KeyguardUpdateMonitor
 
     private val mobileMappings = FakeMobileMappingsProxy()
     private val subscriptionManagerProxy = FakeSubscriptionManagerProxy()
@@ -214,6 +217,7 @@
                 airplaneModeRepository,
                 wifiRepository,
                 fullConnectionFactory,
+                updateMonitor,
             )
 
         testScope.runCurrent()
@@ -1048,6 +1052,7 @@
                     airplaneModeRepository,
                     wifiRepository,
                     fullConnectionFactory,
+                    updateMonitor
                 )
 
             val latest by collectLastValue(underTest.defaultDataSubRatConfig)
@@ -1103,7 +1108,6 @@
     @Test
     fun carrierConfig_initialValueIsFetched() =
         testScope.runTest {
-
             // Value starts out false
             assertThat(underTest.defaultDataSubRatConfig.value.showAtLeast3G).isFalse()
 
@@ -1151,6 +1155,26 @@
             assertThat(latest).isEqualTo(null)
         }
 
+    @Test
+    fun anySimSecure_propagatesStateFromKeyguardUpdateMonitor() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.isAnySimSecure)
+            assertThat(latest).isFalse()
+
+            val updateMonitorCallback = argumentCaptor<KeyguardUpdateMonitorCallback>()
+            verify(updateMonitor).registerCallback(updateMonitorCallback.capture())
+
+            whenever(updateMonitor.isSimPinSecure).thenReturn(true)
+            updateMonitorCallback.value.onSimStateChanged(0, 0, 0)
+
+            assertThat(latest).isTrue()
+
+            whenever(updateMonitor.isSimPinSecure).thenReturn(false)
+            updateMonitorCallback.value.onSimStateChanged(0, 0, 0)
+
+            assertThat(latest).isFalse()
+        }
+
     private fun TestScope.getDefaultNetworkCallback(): ConnectivityManager.NetworkCallback {
         runCurrent()
         val callbackCaptor = argumentCaptor<ConnectivityManager.NetworkCallback>()
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt
index 21a5eb7..28557d3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.broadcast
 
 import android.content.BroadcastReceiver
+import android.content.BroadcastReceiver.PendingResult
 import android.content.Context
 import android.content.Intent
 import android.content.IntentFilter
@@ -28,7 +29,6 @@
 import com.android.systemui.broadcast.logging.BroadcastDispatcherLogger
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.settings.UserTracker
-import java.lang.IllegalStateException
 import java.util.concurrent.ConcurrentHashMap
 import java.util.concurrent.Executor
 
@@ -96,8 +96,14 @@
     /**
      * Sends the given [intent] to *only* the receivers that were registered with an [IntentFilter]
      * that matches the intent.
+     *
+     * A non-null [pendingResult] can be used to pass the sending user.
      */
-    fun sendIntentToMatchingReceiversOnly(context: Context, intent: Intent) {
+    fun sendIntentToMatchingReceiversOnly(
+        context: Context,
+        intent: Intent,
+        pendingResult: PendingResult? = null
+    ) {
         receivers.forEach {
             if (
                 it.filter.match(
@@ -107,6 +113,9 @@
                     /* logTag= */ "FakeBroadcastDispatcher",
                 ) > 0
             ) {
+                if (pendingResult != null) {
+                    it.receiver.pendingResult = pendingResult
+                }
                 it.receiver.onReceive(context, intent)
             }
         }
@@ -130,4 +139,19 @@
         val receiver: BroadcastReceiver,
         val filter: IntentFilter,
     )
+
+    companion object {
+        fun fakePendingResultForUser(userId: Int) =
+            PendingResult(
+                /* resultCode = */ 0,
+                /* resultData = */ "",
+                /* resultExtras = */ null,
+                /* type = */ PendingResult.TYPE_REGISTERED,
+                /* ordered = */ false,
+                /* sticky = */ false,
+                /* token = */ null,
+                userId,
+                /* flags = */ 0,
+            )
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeAutoAddRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeAutoAddRepository.kt
index 9ea079f..57ad2828 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeAutoAddRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeAutoAddRepository.kt
@@ -16,15 +16,16 @@
 
 package com.android.systemui.qs.pipeline.data.repository
 
+import com.android.systemui.qs.pipeline.data.model.RestoreData
 import com.android.systemui.qs.pipeline.shared.TileSpec
-import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
 
 class FakeAutoAddRepository : AutoAddRepository {
 
     private val autoAddedTilesPerUser = mutableMapOf<Int, MutableStateFlow<Set<TileSpec>>>()
 
-    override fun autoAddedTiles(userId: Int): Flow<Set<TileSpec>> {
+    override suspend fun autoAddedTiles(userId: Int): StateFlow<Set<TileSpec>> {
         return getFlow(userId)
     }
 
@@ -39,4 +40,8 @@
 
     private fun getFlow(userId: Int): MutableStateFlow<Set<TileSpec>> =
         autoAddedTilesPerUser.getOrPut(userId) { MutableStateFlow(emptySet()) }
+
+    override suspend fun reconcileRestore(restoreData: RestoreData) {
+        with(getFlow(restoreData.userId)) { value = value + restoreData.restoredAutoAddedTiles }
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeQSSettingsRestoredRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeQSSettingsRestoredRepository.kt
new file mode 100644
index 0000000..e0c2154
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeQSSettingsRestoredRepository.kt
@@ -0,0 +1,16 @@
+package com.android.systemui.qs.pipeline.data.repository
+
+import com.android.systemui.qs.pipeline.data.model.RestoreData
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+
+class FakeQSSettingsRestoredRepository : QSSettingsRestoredRepository {
+    private val _restoreData = MutableSharedFlow<RestoreData>()
+
+    override val restoreData: Flow<RestoreData>
+        get() = _restoreData
+
+    suspend fun onDataRestored(restoreData: RestoreData) {
+        _restoreData.emit(restoreData)
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt
index aa8dbe1..ae4cf3a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.qs.pipeline.data.repository
 
+import com.android.systemui.qs.pipeline.data.model.RestoreData
 import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository.Companion.POSITION_AT_END
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import kotlinx.coroutines.flow.Flow
@@ -26,7 +27,7 @@
 
     private val tilesPerUser = mutableMapOf<Int, MutableStateFlow<List<TileSpec>>>()
 
-    override fun tilesSpecs(userId: Int): Flow<List<TileSpec>> {
+    override suspend fun tilesSpecs(userId: Int): Flow<List<TileSpec>> {
         return getFlow(userId).asStateFlow()
     }
 
@@ -57,4 +58,13 @@
 
     private fun getFlow(userId: Int): MutableStateFlow<List<TileSpec>> =
         tilesPerUser.getOrPut(userId) { MutableStateFlow(emptyList()) }
+
+    override suspend fun reconcileRestore(
+        restoreData: RestoreData,
+        currentAutoAdded: Set<TileSpec>
+    ) {
+        with(getFlow(restoreData.userId)) {
+            value = UserTileSpecRepository.reconcileTiles(value, currentAutoAdded, restoreData)
+        }
+    }
 }
diff --git a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
index a3ccb16..b56b47f 100644
--- a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
+++ b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
@@ -113,6 +113,8 @@
     private final boolean mCrossTaskNavigationAllowedByDefault;
     @NonNull
     private final ArraySet<ComponentName> mCrossTaskNavigationExemptions;
+    @Nullable
+    private final ComponentName mPermissionDialogComponent;
     private final Object mGenericWindowPolicyControllerLock = new Object();
     @Nullable private final ActivityBlockedCallback mActivityBlockedCallback;
     private int mDisplayId = Display.INVALID_DISPLAY;
@@ -171,6 +173,7 @@
             @NonNull Set<ComponentName> activityPolicyExemptions,
             boolean crossTaskNavigationAllowedByDefault,
             @NonNull Set<ComponentName> crossTaskNavigationExemptions,
+            @Nullable ComponentName permissionDialogComponent,
             @Nullable ActivityListener activityListener,
             @Nullable PipBlockedCallback pipBlockedCallback,
             @Nullable ActivityBlockedCallback activityBlockedCallback,
@@ -185,6 +188,7 @@
         mActivityPolicyExemptions = activityPolicyExemptions;
         mCrossTaskNavigationAllowedByDefault = crossTaskNavigationAllowedByDefault;
         mCrossTaskNavigationExemptions = new ArraySet<>(crossTaskNavigationExemptions);
+        mPermissionDialogComponent = permissionDialogComponent;
         mActivityBlockedCallback = activityBlockedCallback;
         setInterestedWindowFlags(windowFlags, systemWindowFlags);
         mActivityListener = activityListener;
@@ -309,6 +313,13 @@
             return false;
         }
 
+        // mPermissionDialogComponent being null means we don't want to block permission Dialogs
+        // based on FLAG_STREAM_PERMISSIONS
+        if (mPermissionDialogComponent != null
+                && mPermissionDialogComponent.equals(activityComponent)) {
+            return false;
+        }
+
         return true;
     }
 
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 203a152..a2e4d2c 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -24,6 +24,7 @@
 import static android.companion.virtual.VirtualDeviceParams.NAVIGATION_POLICY_DEFAULT_ALLOWED;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_ACTIVITY;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS;
+import static android.content.pm.PackageManager.ACTION_REQUEST_PERMISSIONS;
 import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
 
@@ -204,6 +205,7 @@
     @GuardedBy("mVirtualDeviceLock")
     @NonNull
     private final Set<ComponentName> mActivityPolicyExemptions;
+    private final ComponentName mPermissionDialogComponent;
 
     private ActivityListener createListenerAdapter() {
         return new ActivityListener() {
@@ -317,6 +319,11 @@
                 mParams.getVirtualSensorCallback(), mParams.getVirtualSensorConfigs());
         mCameraAccessController = cameraAccessController;
         mCameraAccessController.startObservingIfNeeded();
+        if (!Flags.streamPermissions()) {
+            mPermissionDialogComponent = getPermissionDialogComponent();
+        } else {
+            mPermissionDialogComponent = null;
+        }
         try {
             token.linkToDeath(this, 0);
         } catch (RemoteException e) {
@@ -324,8 +331,14 @@
         }
         mVirtualDeviceLog.logCreated(deviceId, mOwnerUid);
 
-        mPublicVirtualDeviceObject = new VirtualDevice(
-                this, getDeviceId(), getPersistentDeviceId(), mParams.getName());
+        if (Flags.vdmPublicApis()) {
+            mPublicVirtualDeviceObject = new VirtualDevice(
+                    this, getDeviceId(), getPersistentDeviceId(), mParams.getName(),
+                    getDisplayName());
+        } else {
+            mPublicVirtualDeviceObject = new VirtualDevice(
+                    this, getDeviceId(), getPersistentDeviceId(), mParams.getName());
+        }
 
         if (Flags.dynamicPolicy()) {
             mActivityPolicyExemptions = new ArraySet<>(
@@ -951,6 +964,7 @@
                 /*crossTaskNavigationExemptions=*/crossTaskNavigationAllowedByDefault
                         ? mParams.getBlockedCrossTaskNavigations()
                         : mParams.getAllowedCrossTaskNavigations(),
+                mPermissionDialogComponent,
                 createListenerAdapter(),
                 this::onEnteringPipBlocked,
                 this::onActivityBlocked,
@@ -963,6 +977,13 @@
         return gwpc;
     }
 
+    private ComponentName getPermissionDialogComponent() {
+        Intent intent = new Intent(ACTION_REQUEST_PERMISSIONS);
+        PackageManager packageManager = mContext.getPackageManager();
+        intent.setPackage(packageManager.getPermissionControllerPackageName());
+        return intent.resolveActivity(packageManager);
+    }
+
     int createVirtualDisplay(@NonNull VirtualDisplayConfig virtualDisplayConfig,
             @NonNull IVirtualDisplayCallback callback, String packageName) {
         GenericWindowPolicyController gwpc;
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 25ca509c..8cc2665 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -265,17 +265,17 @@
 
         @Override
         public void onUserUnlocking(@NonNull TargetUser user) {
-            mStorageManagerService.onUnlockUser(user.getUserIdentifier());
+            mStorageManagerService.onUserUnlocking(user.getUserIdentifier());
         }
 
         @Override
         public void onUserStopped(@NonNull TargetUser user) {
-            mStorageManagerService.onCleanupUser(user.getUserIdentifier());
+            mStorageManagerService.onUserStopped(user.getUserIdentifier());
         }
 
         @Override
         public void onUserStopping(@NonNull TargetUser user) {
-            mStorageManagerService.onStopUser(user.getUserIdentifier());
+            mStorageManagerService.onUserStopping(user.getUserIdentifier());
         }
 
         @Override
@@ -1163,8 +1163,8 @@
         }
     }
 
-    private void onUnlockUser(int userId) {
-        Slog.d(TAG, "onUnlockUser " + userId);
+    private void onUserUnlocking(int userId) {
+        Slog.d(TAG, "onUserUnlocking " + userId);
 
         if (userId != UserHandle.USER_SYSTEM) {
             // Check if this user shares media with another user
@@ -1227,8 +1227,8 @@
         }
     }
 
-    private void onCleanupUser(int userId) {
-        Slog.d(TAG, "onCleanupUser " + userId);
+    private void onUserStopped(int userId) {
+        Slog.d(TAG, "onUserStopped " + userId);
 
         try {
             mVold.onUserStopped(userId);
@@ -1242,8 +1242,8 @@
         }
     }
 
-    private void onStopUser(int userId) {
-        Slog.i(TAG, "onStopUser " + userId);
+    private void onUserStopping(int userId) {
+        Slog.i(TAG, "onUserStopping " + userId);
         try {
             mStorageSessionController.onUserStopping(userId);
         } catch (Exception e) {
diff --git a/services/core/java/com/android/server/am/AppBatteryTracker.java b/services/core/java/com/android/server/am/AppBatteryTracker.java
index 128bbdf..907069d 100644
--- a/services/core/java/com/android/server/am/AppBatteryTracker.java
+++ b/services/core/java/com/android/server/am/AppBatteryTracker.java
@@ -82,6 +82,7 @@
 import com.android.server.am.AppRestrictionController.UidBatteryUsageProvider;
 import com.android.server.pm.UserManagerInternal;
 
+import java.io.IOException;
 import java.io.PrintWriter;
 import java.lang.reflect.Constructor;
 import java.util.Arrays;
@@ -571,8 +572,18 @@
             builder = new BatteryUsageStatsQuery.Builder()
                     .includeProcessStateData()
                     .aggregateSnapshots(lastUidBatteryUsageStartTs, curStart);
-            updateBatteryUsageStatsOnceInternal(0, buf, builder, userIds, batteryStatsInternal);
+            final BatteryUsageStats statsCommit =
+                    updateBatteryUsageStatsOnceInternal(0,
+                            buf,
+                            builder,
+                            userIds,
+                            batteryStatsInternal);
             curDuration += curStart - lastUidBatteryUsageStartTs;
+            try {
+                statsCommit.close();
+            } catch (IOException e) {
+                Slog.w(TAG, "Failed to close a stat");
+            }
         }
         if (needUpdateUidBatteryUsageInWindow && curDuration >= windowSize) {
             // If we do have long enough data for the window, save it.
@@ -648,8 +659,14 @@
                 }
             }
         }
+        try {
+            stats.close();
+        } catch (IOException e) {
+            Slog.w(TAG, "Failed to close a stat");
+        }
     }
 
+    // The BatteryUsageStats object MUST BE CLOSED when finished using
     private BatteryUsageStats updateBatteryUsageStatsOnceInternal(long expectedDuration,
             SparseArray<BatteryUsage> buf, BatteryUsageStatsQuery.Builder builder,
             ArraySet<UserHandle> userIds, BatteryStatsInternal batteryStatsInternal) {
@@ -662,7 +679,16 @@
             // Shouldn't happen unless in test.
             return null;
         }
+        // We need the first stat in the list, so we should
+        // close out the others.
         final BatteryUsageStats stats = statsList.get(0);
+        for (int i = 1; i < statsList.size(); i++) {
+            try {
+                statsList.get(i).close();
+            } catch (IOException e) {
+                Slog.w(TAG, "Failed to close a stat in BatteryUsageStats List");
+            }
+        }
         final List<UidBatteryConsumer> uidConsumers = stats.getUidBatteryConsumers();
         if (uidConsumers != null) {
             final long start = stats.getStatsStartTimestamp();
diff --git a/services/core/java/com/android/server/am/CoreSettingsObserver.java b/services/core/java/com/android/server/am/CoreSettingsObserver.java
index e84fed7..4b622f5 100644
--- a/services/core/java/com/android/server/am/CoreSettingsObserver.java
+++ b/services/core/java/com/android/server/am/CoreSettingsObserver.java
@@ -173,6 +173,16 @@
                 TextFlags.NAMESPACE, TextFlags.ENABLE_NEW_CONTEXT_MENU,
                 TextFlags.KEY_ENABLE_NEW_CONTEXT_MENU, boolean.class,
                 TextFlags.ENABLE_NEW_CONTEXT_MENU_DEFAULT));
+
+        // Register all text aconfig flags.
+        for (String flag : TextFlags.TEXT_ACONFIGS_FLAGS) {
+            sDeviceConfigEntries.add(new DeviceConfigEntry<Boolean>(
+                    TextFlags.NAMESPACE,
+                    flag,
+                    TextFlags.getKeyForFlag(flag),
+                    boolean.class,
+                    false));  // All aconfig flags are false by default.
+        }
         // add other device configs here...
     }
     private static volatile boolean sDeviceConfigContextEntriesLoaded = false;
diff --git a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java
index 99a5398..debf828 100644
--- a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java
+++ b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java
@@ -33,6 +33,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.display.BrightnessSynchronizer;
+import com.android.internal.display.BrightnessUtils;
 import com.android.internal.util.Preconditions;
 import com.android.server.display.utils.Plog;
 import com.android.server.display.whitebalance.DisplayWhiteBalanceController;
diff --git a/services/core/java/com/android/server/display/BrightnessRangeController.java b/services/core/java/com/android/server/display/BrightnessRangeController.java
index 13ee47e..40b2f5a 100644
--- a/services/core/java/com/android/server/display/BrightnessRangeController.java
+++ b/services/core/java/com/android/server/display/BrightnessRangeController.java
@@ -43,14 +43,16 @@
 
     BrightnessRangeController(HighBrightnessModeController hbmController,
             Runnable modeChangeCallback, DisplayDeviceConfig displayDeviceConfig, Handler handler,
-            DisplayManagerFlags flags) {
+            DisplayManagerFlags flags, IBinder displayToken, DisplayDeviceInfo info) {
         this(hbmController, modeChangeCallback, displayDeviceConfig,
-                new HdrClamper(modeChangeCallback::run, new Handler(handler.getLooper())), flags);
+                new HdrClamper(modeChangeCallback::run, new Handler(handler.getLooper())), flags,
+                displayToken, info);
     }
 
     BrightnessRangeController(HighBrightnessModeController hbmController,
             Runnable modeChangeCallback, DisplayDeviceConfig displayDeviceConfig,
-            HdrClamper hdrClamper, DisplayManagerFlags flags) {
+            HdrClamper hdrClamper, DisplayManagerFlags flags, IBinder displayToken,
+            DisplayDeviceInfo info) {
         mHbmController = hbmController;
         mModeChangeCallback = modeChangeCallback;
         mHdrClamper = hdrClamper;
@@ -60,10 +62,7 @@
             mNormalBrightnessModeController.resetNbmData(
                     displayDeviceConfig.getLuxThrottlingData());
         }
-        if (mUseHdrClamper) {
-            mHdrClamper.resetHdrConfig(displayDeviceConfig.getHdrBrightnessData());
-        }
-
+        updateHdrClamper(info, displayToken, displayDeviceConfig);
     }
 
     void dump(PrintWriter pw) {
@@ -101,13 +100,12 @@
                             displayDeviceConfig::getHdrBrightnessFromSdr);
                 }
         );
-        if (mUseHdrClamper) {
-            mHdrClamper.resetHdrConfig(displayDeviceConfig.getHdrBrightnessData());
-        }
+        updateHdrClamper(info, token, displayDeviceConfig);
     }
 
     void stop() {
         mHbmController.stop();
+        mHdrClamper.stop();
     }
 
     void setAutoBrightnessEnabled(int state) {
@@ -151,6 +149,18 @@
         return mHbmController.getTransitionPoint();
     }
 
+    private void updateHdrClamper(DisplayDeviceInfo info, IBinder token,
+            DisplayDeviceConfig displayDeviceConfig) {
+        if (mUseHdrClamper) {
+            DisplayDeviceConfig.HighBrightnessModeData hbmData =
+                    displayDeviceConfig.getHighBrightnessModeData();
+            float minimumHdrPercentOfScreen =
+                    hbmData == null ? -1f : hbmData.minimumHdrPercentOfScreen;
+            mHdrClamper.resetHdrConfig(displayDeviceConfig.getHdrBrightnessData(), info.width,
+                    info.height, minimumHdrPercentOfScreen, token);
+        }
+    }
+
     private void applyChanges(BooleanSupplier nbmChangesFunc, Runnable hbmChangesFunc) {
         if (mUseNbmController) {
             boolean nbmTransitionChanged = nbmChangesFunc.getAsBoolean();
diff --git a/services/core/java/com/android/server/display/DisplayDevice.java b/services/core/java/com/android/server/display/DisplayDevice.java
index 8642fb8..098cb87 100644
--- a/services/core/java/com/android/server/display/DisplayDevice.java
+++ b/services/core/java/com/android/server/display/DisplayDevice.java
@@ -23,6 +23,7 @@
 import android.content.Context;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.hardware.display.DisplayManagerInternal;
 import android.hardware.display.DisplayViewport;
 import android.os.IBinder;
 import android.util.Slog;
@@ -205,6 +206,24 @@
      */
     public Runnable requestDisplayStateLocked(int state, float brightnessState,
             float sdrBrightnessState) {
+        return requestDisplayStateLocked(state, brightnessState, sdrBrightnessState, null);
+    }
+
+    /**
+     * Sets the display state, if supported.
+     *
+     * @param state The new display state.
+     * @param brightnessState The new display brightnessState.
+     * @param sdrBrightnessState The new display brightnessState for SDR layers.
+     * @param displayOffloadSession {@link DisplayOffloadSession} associated with current device.
+     * @return A runnable containing work to be deferred until after we have exited the critical
+     *     section, or null if none.
+     */
+    public Runnable requestDisplayStateLocked(
+            int state,
+            float brightnessState,
+            float sdrBrightnessState,
+            @Nullable DisplayManagerInternal.DisplayOffloadSession displayOffloadSession) {
         return null;
     }
 
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 9ef84cb..e942c17 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -1772,7 +1772,7 @@
         synchronized (mSyncRoot) {
             // main display adapter
             registerDisplayAdapterLocked(mInjector.getLocalDisplayAdapter(mSyncRoot, mContext,
-                    mHandler, mDisplayDeviceRepo));
+                    mHandler, mDisplayDeviceRepo, mFlags));
 
             // Standalone VR devices rely on a virtual display as their primary display for
             // 2D UI. We register virtual display adapter along side the main display adapter
@@ -2093,8 +2093,11 @@
             // Only send a request for display state if display state has already been initialized.
             if (state != Display.STATE_UNKNOWN) {
                 final BrightnessPair brightnessPair = mDisplayBrightnesses.get(displayId);
-                return device.requestDisplayStateLocked(state, brightnessPair.brightness,
-                        brightnessPair.sdrBrightness);
+                return device.requestDisplayStateLocked(
+                        state,
+                        brightnessPair.brightness,
+                        brightnessPair.sdrBrightness,
+                        display.getDisplayOffloadSessionLocked());
             }
         }
         return null;
@@ -3183,9 +3186,10 @@
         }
 
         LocalDisplayAdapter getLocalDisplayAdapter(SyncRoot syncRoot, Context context,
-                                                   Handler handler,
-                                                   DisplayAdapter.Listener displayAdapterListener) {
-            return new LocalDisplayAdapter(syncRoot, context, handler, displayAdapterListener);
+                Handler handler, DisplayAdapter.Listener displayAdapterListener,
+                DisplayManagerFlags flags) {
+            return new LocalDisplayAdapter(syncRoot, context, handler, displayAdapterListener,
+                    flags);
         }
 
         long getDefaultDisplayDelayTimeout() {
@@ -4806,6 +4810,49 @@
             }
             return displayGroupIds;
         }
+
+        @Override
+        public DisplayManagerInternal.DisplayOffloadSession registerDisplayOffloader(
+                int displayId, @NonNull DisplayManagerInternal.DisplayOffloader displayOffloader) {
+            if (!mFlags.isDisplayOffloadEnabled()) {
+                return null;
+            }
+            synchronized (mSyncRoot) {
+                LogicalDisplay logicalDisplay = mLogicalDisplayMapper.getDisplayLocked(displayId);
+                if (logicalDisplay == null) {
+                    Slog.w(TAG, "registering DisplayOffloader: LogicalDisplay for displayId="
+                            + displayId + " is not found. No Op.");
+                    return null;
+                }
+
+                DisplayPowerControllerInterface displayPowerController =
+                        mDisplayPowerControllers.get(logicalDisplay.getDisplayIdLocked());
+                if (displayPowerController == null) {
+                    Slog.w(TAG,
+                            "setting doze state override: DisplayPowerController for displayId="
+                                    + displayId + " is unavailable. No Op.");
+                    return null;
+                }
+
+                DisplayOffloadSession session =
+                        new DisplayOffloadSession() {
+                            @Override
+                            public void setDozeStateOverride(int displayState) {
+                                synchronized (mSyncRoot) {
+                                    displayPowerController.overrideDozeScreenState(displayState);
+                                }
+                            }
+
+                            @Override
+                            public DisplayOffloader getDisplayOffloader() {
+                                return displayOffloader;
+                            }
+                        };
+                logicalDisplay.setDisplayOffloadSessionLocked(session);
+                displayPowerController.setDisplayOffloadSession(session);
+                return session;
+            }
+        }
     }
 
     class DesiredDisplayModeSpecsObserver
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 83f4df9..ce98559 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -34,6 +34,8 @@
 import android.hardware.display.BrightnessChangeEvent;
 import android.hardware.display.BrightnessConfiguration;
 import android.hardware.display.BrightnessInfo;
+import android.hardware.display.DisplayManagerInternal;
+import android.hardware.display.DisplayManagerInternal.DisplayOffloadSession;
 import android.hardware.display.DisplayManagerInternal.DisplayPowerCallbacks;
 import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
 import android.metrics.LogMaker;
@@ -588,8 +590,9 @@
             new SparseArray<>();
 
     private boolean mBootCompleted;
-
     private final DisplayManagerFlags mFlags;
+    private int mDozeStateOverride = Display.STATE_UNKNOWN;
+    private DisplayManagerInternal.DisplayOffloadSession mDisplayOffloadSession;
 
     /**
      * Creates the display power controller.
@@ -684,7 +687,9 @@
         HighBrightnessModeController hbmController = createHbmControllerLocked(modeChangeCallback);
 
         mBrightnessRangeController = new BrightnessRangeController(hbmController,
-                modeChangeCallback, mDisplayDeviceConfig, mHandler, flags);
+                modeChangeCallback, mDisplayDeviceConfig, mHandler, flags,
+                mDisplayDevice.getDisplayTokenLocked(),
+                mDisplayDevice.getDisplayDeviceInfoLocked());
 
         mBrightnessThrottler = createBrightnessThrottlerLocked();
 
@@ -957,6 +962,23 @@
     }
 
     @Override
+    public void overrideDozeScreenState(int displayState) {
+        synchronized (mLock) {
+            if (mDisplayOffloadSession == null ||
+                    !DisplayOffloadSession.isSupportedOffloadState(displayState)) {
+                return;
+            }
+            mDozeStateOverride = displayState;
+            sendUpdatePowerState();
+        }
+    }
+
+    @Override
+    public void setDisplayOffloadSession(DisplayOffloadSession session) {
+        mDisplayOffloadSession = session;
+    }
+
+    @Override
     public BrightnessConfiguration getDefaultBrightnessConfiguration() {
         if (mAutomaticBrightnessController == null) {
             return null;
@@ -1518,6 +1540,7 @@
                 } else {
                     state = Display.STATE_DOZE;
                 }
+                state = mDozeStateOverride == Display.STATE_UNKNOWN ? state : mDozeStateOverride;
                 if (!mAllowAutoBrightnessWhileDozingConfig) {
                     brightnessState = mPowerRequest.dozeScreenBrightness;
                     mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE);
@@ -1937,6 +1960,7 @@
                 // We want to scale HDR brightness level with the SDR level, we also need to restore
                 // SDR brightness immediately when entering dim or low power mode.
                 animateValue = mBrightnessRangeController.getHdrBrightnessValue();
+                mBrightnessReasonTemp.addModifier(BrightnessReason.MODIFIER_HDR);
             }
 
             final float currentBrightness = mPowerState.getScreenBrightness();
@@ -3001,6 +3025,7 @@
             pw.println("  mLeadDisplayId=" + mLeadDisplayId);
             pw.println("  mLightSensor=" + mLightSensor);
             pw.println("  mDisplayBrightnessFollowers=" + mDisplayBrightnessFollowers);
+            pw.println("  mDozeStateOverride=" + mDozeStateOverride);
 
             pw.println();
             pw.println("Display Power Controller Locked State:");
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index b0d293a..1652871 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -31,6 +31,8 @@
 import android.hardware.display.BrightnessChangeEvent;
 import android.hardware.display.BrightnessConfiguration;
 import android.hardware.display.BrightnessInfo;
+import android.hardware.display.DisplayManagerInternal;
+import android.hardware.display.DisplayManagerInternal.DisplayOffloadSession;
 import android.hardware.display.DisplayManagerInternal.DisplayPowerCallbacks;
 import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
 import android.metrics.LogMaker;
@@ -470,9 +472,10 @@
             new SparseArray();
 
     private boolean mBootCompleted;
-
     private final DisplayManagerFlags mFlags;
 
+    private DisplayManagerInternal.DisplayOffloadSession mDisplayOffloadSession;
+
     /**
      * Creates the display power controller.
      */
@@ -549,7 +552,9 @@
         mBrightnessThrottler = createBrightnessThrottlerLocked();
 
         mBrightnessRangeController = mInjector.getBrightnessRangeController(hbmController,
-                modeChangeCallback, mDisplayDeviceConfig, mHandler, flags);
+                modeChangeCallback, mDisplayDeviceConfig, mHandler, flags,
+                mDisplayDevice.getDisplayTokenLocked(),
+                mDisplayDevice.getDisplayDeviceInfoLocked());
 
         mDisplayBrightnessController =
                 new DisplayBrightnessController(context, null,
@@ -588,21 +593,24 @@
 
         if (mDisplayId == Display.DEFAULT_DISPLAY) {
             mCdsi = LocalServices.getService(ColorDisplayServiceInternal.class);
-            boolean active = mCdsi.setReduceBrightColorsListener(new ReduceBrightColorsListener() {
-                @Override
-                public void onReduceBrightColorsActivationChanged(boolean activated,
-                        boolean userInitiated) {
-                    applyReduceBrightColorsSplineAdjustment();
+            if (mCdsi != null) {
+                boolean active = mCdsi.setReduceBrightColorsListener(
+                        new ReduceBrightColorsListener() {
+                            @Override
+                            public void onReduceBrightColorsActivationChanged(boolean activated,
+                                    boolean userInitiated) {
+                                applyReduceBrightColorsSplineAdjustment();
 
-                }
+                            }
 
-                @Override
-                public void onReduceBrightColorsStrengthChanged(int strength) {
+                            @Override
+                            public void onReduceBrightColorsStrengthChanged(int strength) {
+                                applyReduceBrightColorsSplineAdjustment();
+                            }
+                        });
+                if (active) {
                     applyReduceBrightColorsSplineAdjustment();
                 }
-            });
-            if (active) {
-                applyReduceBrightColorsSplineAdjustment();
             }
         } else {
             mCdsi = null;
@@ -760,6 +768,24 @@
     }
 
     @Override
+    public void overrideDozeScreenState(int displayState) {
+        mHandler.postAtTime(() -> {
+            if (mDisplayOffloadSession == null
+                    || !(DisplayOffloadSession.isSupportedOffloadState(displayState)
+                    || displayState == Display.STATE_UNKNOWN)) {
+                return;
+            }
+            mDisplayStateController.overrideDozeScreenState(displayState);
+            sendUpdatePowerState();
+        }, mClock.uptimeMillis());
+    }
+
+    @Override
+    public void setDisplayOffloadSession(DisplayOffloadSession session) {
+        mDisplayOffloadSession = session;
+    }
+
+    @Override
     public BrightnessConfiguration getDefaultBrightnessConfiguration() {
         if (mAutomaticBrightnessController == null) {
             return null;
@@ -1540,6 +1566,7 @@
                 // SDR brightness immediately when entering dim or low power mode.
                 animateValue = mBrightnessRangeController.getHdrBrightnessValue();
                 customTransitionRate = mBrightnessRangeController.getHdrTransitionRate();
+                mBrightnessReasonTemp.addModifier(BrightnessReason.MODIFIER_HDR);
             }
 
             final float currentBrightness = mPowerState.getScreenBrightness();
@@ -1871,15 +1898,12 @@
 
     private HighBrightnessModeController createHbmControllerLocked(
             HighBrightnessModeMetadata hbmMetadata, Runnable modeChangeCallback) {
-        final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
-        final DisplayDeviceConfig ddConfig = device.getDisplayDeviceConfig();
-        final IBinder displayToken =
-                mLogicalDisplay.getPrimaryDisplayDeviceLocked().getDisplayTokenLocked();
-        final String displayUniqueId =
-                mLogicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId();
+        final DisplayDeviceConfig ddConfig = mDisplayDevice.getDisplayDeviceConfig();
+        final IBinder displayToken = mDisplayDevice.getDisplayTokenLocked();
+        final String displayUniqueId = mDisplayDevice.getUniqueId();
         final DisplayDeviceConfig.HighBrightnessModeData hbmData =
                 ddConfig != null ? ddConfig.getHighBrightnessModeData() : null;
-        final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();
+        final DisplayDeviceInfo info = mDisplayDevice.getDisplayDeviceInfoLocked();
         return mInjector.getHighBrightnessModeController(mHandler, info.width, info.height,
                 displayToken, displayUniqueId, PowerManager.BRIGHTNESS_MIN,
                 PowerManager.BRIGHTNESS_MAX, hbmData, (sdrBrightness, maxDesiredHdrSdrRatio) ->
@@ -3025,9 +3049,9 @@
         BrightnessRangeController getBrightnessRangeController(
                 HighBrightnessModeController hbmController, Runnable modeChangeCallback,
                 DisplayDeviceConfig displayDeviceConfig, Handler handler,
-                DisplayManagerFlags flags) {
+                DisplayManagerFlags flags, IBinder displayToken, DisplayDeviceInfo info) {
             return new BrightnessRangeController(hbmController,
-                    modeChangeCallback, displayDeviceConfig, handler, flags);
+                    modeChangeCallback, displayDeviceConfig, handler, flags, displayToken, info);
         }
 
         DisplayWhiteBalanceController getDisplayWhiteBalanceController(Handler handler,
diff --git a/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java b/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java
index e3108c9..181386a 100644
--- a/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java
+++ b/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java
@@ -139,6 +139,10 @@
     boolean requestPowerState(DisplayManagerInternal.DisplayPowerRequest request,
             boolean waitForNegativeProximity);
 
+    void overrideDozeScreenState(int displayState);
+
+    void setDisplayOffloadSession(DisplayManagerInternal.DisplayOffloadSession session);
+
     /**
      * Sets up the temporary autobrightness adjustment when the user is yet to settle down to a
      * value.
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index 0f3e7c5..0a1f316 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -22,6 +22,9 @@
 import android.app.ActivityThread;
 import android.content.Context;
 import android.content.res.Resources;
+import android.hardware.display.DisplayManagerInternal;
+import android.hardware.display.DisplayManagerInternal.DisplayOffloader;
+import android.hardware.display.DisplayManagerInternal.DisplayOffloadSession;
 import android.hardware.sidekick.SidekickInternal;
 import android.os.Build;
 import android.os.Handler;
@@ -47,6 +50,7 @@
 import com.android.internal.display.BrightnessSynchronizer;
 import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.server.LocalServices;
+import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.mode.DisplayModeDirector;
 import com.android.server.lights.LightsManager;
 import com.android.server.lights.LogicalLight;
@@ -80,21 +84,24 @@
 
     private final boolean mIsBootDisplayModeSupported;
 
+    private final DisplayManagerFlags mFlags;
+
     private Context mOverlayContext;
 
     // Called with SyncRoot lock held.
     LocalDisplayAdapter(DisplayManagerService.SyncRoot syncRoot, Context context,
-            Handler handler, Listener listener) {
-        this(syncRoot, context, handler, listener, new Injector());
+            Handler handler, Listener listener, DisplayManagerFlags flags) {
+        this(syncRoot, context, handler, listener, flags, new Injector());
     }
 
     @VisibleForTesting
     LocalDisplayAdapter(DisplayManagerService.SyncRoot syncRoot, Context context, Handler handler,
-            Listener listener, Injector injector) {
+            Listener listener, DisplayManagerFlags flags, Injector injector) {
         super(syncRoot, context, handler, listener, TAG);
         mInjector = injector;
         mSurfaceControlProxy = mInjector.getSurfaceControlProxy();
         mIsBootDisplayModeSupported = mSurfaceControlProxy.getBootDisplayModeSupport();
+        mFlags = flags;
     }
 
     @Override
@@ -224,6 +231,7 @@
         private boolean mAllmRequested;
         private boolean mGameContentTypeRequested;
         private boolean mSidekickActive;
+        private boolean mDisplayOffloadActive;
         private SurfaceControl.StaticDisplayInfo mStaticDisplayInfo;
         // The supported display modes according to SurfaceFlinger
         private SurfaceControl.DisplayMode[] mSfDisplayModes;
@@ -746,8 +754,12 @@
         }
 
         @Override
-        public Runnable requestDisplayStateLocked(final int state, final float brightnessState,
-                final float sdrBrightnessState) {
+        public Runnable requestDisplayStateLocked(
+                final int state,
+                final float brightnessState,
+                final float sdrBrightnessState,
+                DisplayOffloadSession displayOffloadSession) {
+
             // Assume that the brightness is off if the display is being turned off.
             assert state != Display.STATE_OFF
                     || brightnessState == PowerManager.BRIGHTNESS_OFF_FLOAT;
@@ -813,18 +825,39 @@
                                     + ", state=" + Display.stateToString(state) + ")");
                         }
 
-                        // We must tell sidekick to stop controlling the display before we
-                        // can change its power mode, so do that first.
-                        if (mSidekickActive) {
-                            Trace.traceBegin(Trace.TRACE_TAG_POWER,
-                                    "SidekickInternal#endDisplayControl");
-                            try {
-                                mSidekickInternal.endDisplayControl();
-                            } finally {
-                                Trace.traceEnd(Trace.TRACE_TAG_POWER);
+                        DisplayOffloader displayOffloader =
+                                displayOffloadSession == null
+                                        ? null
+                                        : displayOffloadSession.getDisplayOffloader();
+
+                        boolean isDisplayOffloadEnabled = mFlags.isDisplayOffloadEnabled();
+
+                        // We must tell sidekick/displayoffload to stop controlling the display
+                        // before we can change its power mode, so do that first.
+                        if (isDisplayOffloadEnabled) {
+                            if (mDisplayOffloadActive && displayOffloader != null) {
+                                Trace.traceBegin(Trace.TRACE_TAG_POWER,
+                                        "DisplayOffloader#stopOffload");
+                                try {
+                                    displayOffloader.stopOffload();
+                                } finally {
+                                    Trace.traceEnd(Trace.TRACE_TAG_POWER);
+                                }
+                                mDisplayOffloadActive = false;
                             }
-                            mSidekickActive = false;
+                        } else {
+                            if (mSidekickActive) {
+                                Trace.traceBegin(Trace.TRACE_TAG_POWER,
+                                        "SidekickInternal#endDisplayControl");
+                                try {
+                                    mSidekickInternal.endDisplayControl();
+                                } finally {
+                                    Trace.traceEnd(Trace.TRACE_TAG_POWER);
+                                }
+                                mSidekickActive = false;
+                            }
                         }
+
                         final int mode = getPowerModeForState(state);
                         Trace.traceBegin(Trace.TRACE_TAG_POWER, "setDisplayState("
                                 + "id=" + physicalDisplayId
@@ -836,16 +869,32 @@
                             Trace.traceEnd(Trace.TRACE_TAG_POWER);
                         }
                         setCommittedState(state);
+
                         // If we're entering a suspended (but not OFF) power state and we
-                        // have a sidekick available, tell it now that it can take control.
-                        if (Display.isSuspendedState(state) && state != Display.STATE_OFF
-                                && mSidekickInternal != null && !mSidekickActive) {
-                            Trace.traceBegin(Trace.TRACE_TAG_POWER,
-                                    "SidekickInternal#startDisplayControl");
-                            try {
-                                mSidekickActive = mSidekickInternal.startDisplayControl(state);
-                            } finally {
-                                Trace.traceEnd(Trace.TRACE_TAG_POWER);
+                        // have a sidekick/displayoffload available, tell it now that it can take
+                        // control.
+                        if (isDisplayOffloadEnabled) {
+                            if (DisplayOffloadSession.isSupportedOffloadState(state) &&
+                                    displayOffloader != null
+                                    && !mDisplayOffloadActive) {
+                                Trace.traceBegin(
+                                        Trace.TRACE_TAG_POWER, "DisplayOffloader#startOffload");
+                                try {
+                                    mDisplayOffloadActive = displayOffloader.startOffload();
+                                } finally {
+                                    Trace.traceEnd(Trace.TRACE_TAG_POWER);
+                                }
+                            }
+                        } else {
+                            if (Display.isSuspendedState(state) && state != Display.STATE_OFF
+                                    && mSidekickInternal != null && !mSidekickActive) {
+                                Trace.traceBegin(Trace.TRACE_TAG_POWER,
+                                        "SidekickInternal#startDisplayControl");
+                                try {
+                                    mSidekickActive = mSidekickInternal.startDisplayControl(state);
+                                } finally {
+                                    Trace.traceEnd(Trace.TRACE_TAG_POWER);
+                                }
                             }
                         }
                     }
@@ -858,6 +907,7 @@
                         }
                     }
 
+
                     private void setDisplayBrightness(float brightnessState,
                             float sdrBrightnessState) {
                         // brightnessState includes invalid, off and full range.
diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java
index d4d104e..bd82b81 100644
--- a/services/core/java/com/android/server/display/LogicalDisplay.java
+++ b/services/core/java/com/android/server/display/LogicalDisplay.java
@@ -137,6 +137,9 @@
     private final Rect mTempLayerStackRect = new Rect();
     private final Rect mTempDisplayRect = new Rect();
 
+    /** A session token that controls the offloading operations of this logical display. */
+    private DisplayManagerInternal.DisplayOffloadSession mDisplayOffloadSession;
+
     /**
      * Name of a display group to which the display is assigned.
      */
@@ -941,6 +944,15 @@
         return mDisplayGroupName;
     }
 
+    public void setDisplayOffloadSessionLocked(
+            DisplayManagerInternal.DisplayOffloadSession session) {
+        mDisplayOffloadSession = session;
+    }
+
+    public DisplayManagerInternal.DisplayOffloadSession getDisplayOffloadSessionLocked() {
+        return mDisplayOffloadSession;
+    }
+
     public void dumpLocked(PrintWriter pw) {
         pw.println("mDisplayId=" + mDisplayId);
         pw.println("mIsEnabled=" + mIsEnabled);
diff --git a/services/core/java/com/android/server/display/RampAnimator.java b/services/core/java/com/android/server/display/RampAnimator.java
index 5ba042c..e38c2c5 100644
--- a/services/core/java/com/android/server/display/RampAnimator.java
+++ b/services/core/java/com/android/server/display/RampAnimator.java
@@ -20,6 +20,8 @@
 import android.util.FloatProperty;
 import android.view.Choreographer;
 
+import com.android.internal.display.BrightnessUtils;
+
 /**
  * A custom animator that progressively updates a property value at
  * a given variable rate until it reaches a particular target value.
diff --git a/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java b/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java
index a514136..e46edd9 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java
@@ -17,9 +17,13 @@
 package com.android.server.display.brightness.clamper;
 
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.PowerManager;
+import android.view.SurfaceControlHdrLayerInfoListener;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.display.config.HdrBrightnessData;
 
 import java.io.PrintWriter;
@@ -33,11 +37,18 @@
 
     private final Runnable mDebouncer;
 
+    private final HdrLayerInfoListener mHdrListener;
+
     @Nullable
     private HdrBrightnessData mHdrBrightnessData = null;
 
+    @Nullable
+    private IBinder mRegisteredDisplayToken = null;
+
     private float mAmbientLux = Float.MAX_VALUE;
 
+    private boolean mHdrVisible = false;
+
     private float mMaxBrightness = PowerManager.BRIGHTNESS_MAX;
     private float mDesiredMaxBrightness = PowerManager.BRIGHTNESS_MAX;
 
@@ -47,6 +58,12 @@
 
     public HdrClamper(BrightnessClamperController.ClamperChangeListener clamperChangeListener,
             Handler handler) {
+        this(clamperChangeListener, handler, new Injector());
+    }
+
+    @VisibleForTesting
+    public HdrClamper(BrightnessClamperController.ClamperChangeListener clamperChangeListener,
+            Handler handler, Injector injector) {
         mClamperChangeListener = clamperChangeListener;
         mHandler = handler;
         mDebouncer = () -> {
@@ -54,6 +71,10 @@
             mMaxBrightness = mDesiredMaxBrightness;
             mClamperChangeListener.onChanged();
         };
+        mHdrListener = injector.getHdrListener((visible) -> {
+            mHdrVisible = visible;
+            recalculateBrightnessCap(mHdrBrightnessData, mAmbientLux, mHdrVisible);
+        }, handler);
     }
 
     // Called in same looper: mHandler.getLooper()
@@ -72,16 +93,37 @@
      */
     public void onAmbientLuxChange(float ambientLux) {
         mAmbientLux = ambientLux;
-        recalculateBrightnessCap(mHdrBrightnessData, ambientLux);
+        recalculateBrightnessCap(mHdrBrightnessData, ambientLux, mHdrVisible);
     }
 
     /**
      * Updates brightness cap config.
      * Called in same looper: mHandler.getLooper()
      */
-    public void resetHdrConfig(HdrBrightnessData data) {
+    @SuppressLint("AndroidFrameworkRequiresPermission")
+    public void resetHdrConfig(HdrBrightnessData data, int width, int height,
+            float minimumHdrPercentOfScreen, IBinder displayToken) {
         mHdrBrightnessData = data;
-        recalculateBrightnessCap(data, mAmbientLux);
+        mHdrListener.mHdrMinPixels = (float) (width * height) * minimumHdrPercentOfScreen;
+        if (displayToken != mRegisteredDisplayToken) { // token changed, resubscribe
+            if (mRegisteredDisplayToken != null) { // previous token not null, unsubscribe
+                mHdrListener.unregister(mRegisteredDisplayToken);
+                mHdrVisible = false;
+            }
+            if (displayToken != null) { // new token not null, subscribe
+                mHdrListener.register(displayToken);
+            }
+            mRegisteredDisplayToken = displayToken;
+        }
+        recalculateBrightnessCap(data, mAmbientLux, mHdrVisible);
+    }
+
+    /** Clean up all resources */
+    @SuppressLint("AndroidFrameworkRequiresPermission")
+    public void stop() {
+        if (mRegisteredDisplayToken != null) {
+            mHdrListener.unregister(mRegisteredDisplayToken);
+        }
     }
 
     /**
@@ -98,13 +140,28 @@
         pw.println("  mAmbientLux=" + mAmbientLux);
     }
 
-    private void recalculateBrightnessCap(HdrBrightnessData data, float ambientLux) {
-        if (data == null) {
-            mHandler.removeCallbacks(mDebouncer);
+    private void reset() {
+        if (mMaxBrightness == PowerManager.BRIGHTNESS_MAX
+                && mDesiredMaxBrightness == PowerManager.BRIGHTNESS_MAX && mTransitionRate == -1f
+                && mDesiredTransitionRate == -1f) { // already done reset, do nothing
             return;
         }
-        float expectedMaxBrightness = findBrightnessLimit(data, ambientLux);
+        mHandler.removeCallbacks(mDebouncer);
+        mMaxBrightness = PowerManager.BRIGHTNESS_MAX;
+        mDesiredMaxBrightness = PowerManager.BRIGHTNESS_MAX;
+        mDesiredTransitionRate = -1f;
+        mTransitionRate = 1f;
+        mClamperChangeListener.onChanged();
+    }
 
+    private void recalculateBrightnessCap(HdrBrightnessData data, float ambientLux,
+            boolean hdrVisible) {
+        if (data == null || !hdrVisible) {
+            reset();
+            return;
+        }
+
+        float expectedMaxBrightness = findBrightnessLimit(data, ambientLux);
         if (mMaxBrightness == expectedMaxBrightness) {
             mDesiredMaxBrightness = mMaxBrightness;
             mDesiredTransitionRate = -1f;
@@ -127,6 +184,8 @@
             mHandler.removeCallbacks(mDebouncer);
             mHandler.postDelayed(mDebouncer, debounceTime);
         }
+        // do nothing if expectedMaxBrightness == mDesiredMaxBrightness
+        // && expectedMaxBrightness != mMaxBrightness
     }
 
     private float findBrightnessLimit(HdrBrightnessData data, float ambientLux) {
@@ -143,4 +202,36 @@
         }
         return foundMaxBrightness;
     }
+
+    @FunctionalInterface
+    interface HdrListener {
+        void onHdrVisible(boolean visible);
+    }
+
+    static class HdrLayerInfoListener extends SurfaceControlHdrLayerInfoListener {
+        private final HdrListener mHdrListener;
+
+        private final Handler mHandler;
+
+        private float mHdrMinPixels = Float.MAX_VALUE;
+
+        HdrLayerInfoListener(HdrListener hdrListener, Handler handler) {
+            mHdrListener = hdrListener;
+            mHandler = handler;
+        }
+
+        @Override
+        public void onHdrInfoChanged(IBinder displayToken, int numberOfHdrLayers, int maxW,
+                int maxH, int flags, float maxDesiredHdrSdrRatio) {
+            mHandler.post(() ->
+                    mHdrListener.onHdrVisible(
+                            numberOfHdrLayers > 0 && (float) (maxW * maxH) >= mHdrMinPixels));
+        }
+    }
+
+    static class Injector {
+        HdrLayerInfoListener getHdrListener(HdrListener hdrListener, Handler handler) {
+            return new HdrLayerInfoListener(hdrListener, handler);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index ff768d6..b6273e1 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -47,6 +47,10 @@
             Flags.FLAG_ENABLE_ADAPTIVE_TONE_IMPROVEMENTS_1,
             Flags::enableAdaptiveToneImprovements1);
 
+    private final FlagState mDisplayOffloadFlagState = new FlagState(
+            Flags.FLAG_ENABLE_DISPLAY_OFFLOAD,
+            Flags::enableDisplayOffload);
+
     private final FlagState mDisplayResolutionRangeVotingState = new FlagState(
             Flags.FLAG_ENABLE_DISPLAY_RESOLUTION_RANGE_VOTING,
             Flags::enableDisplayResolutionRangeVoting);
@@ -111,6 +115,11 @@
         return mDisplaysRefreshRatesSynchronizationState.isEnabled();
     }
 
+    /** Returns whether displayoffload is enabled on not */
+    public boolean isDisplayOffloadEnabled() {
+        return mDisplayOffloadFlagState.isEnabled();
+    }
+
     private static class FlagState {
 
         private final String mName;
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index a5b8cbb..542f26c 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -63,5 +63,12 @@
     namespace: "display_manager"
     description: "Enables synchronization of refresh rates across displays"
     bug: "294015845"
+}
+
+flag {
+    name: "enable_display_offload"
+    namespace: "display_manager"
+    description: "Feature flag for DisplayOffload"
+    bug: "299521647"
     is_fixed_read_only: true
 }
diff --git a/services/core/java/com/android/server/display/state/DisplayStateController.java b/services/core/java/com/android/server/display/state/DisplayStateController.java
index b1a1c60..5d6e650 100644
--- a/services/core/java/com/android/server/display/state/DisplayStateController.java
+++ b/services/core/java/com/android/server/display/state/DisplayStateController.java
@@ -32,6 +32,7 @@
 public class DisplayStateController {
     private DisplayPowerProximityStateController mDisplayPowerProximityStateController;
     private boolean mPerformScreenOffTransition = false;
+    private int mDozeStateOverride = Display.STATE_UNKNOWN;
 
     public DisplayStateController(DisplayPowerProximityStateController
             displayPowerProximityStateController) {
@@ -65,6 +66,7 @@
                 } else {
                     state = Display.STATE_DOZE;
                 }
+                state = mDozeStateOverride == Display.STATE_UNKNOWN ? state : mDozeStateOverride;
                 break;
             case DisplayManagerInternal.DisplayPowerRequest.POLICY_DIM:
             case DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT:
@@ -84,6 +86,10 @@
         return state;
     }
 
+    public void overrideDozeScreenState(int displayState) {
+        mDozeStateOverride = displayState;
+    }
+
     /**
      * Checks if the screen off transition is to be performed or not.
      */
@@ -100,6 +106,8 @@
         pw.println();
         pw.println("DisplayStateController:");
         pw.println("  mPerformScreenOffTransition:" + mPerformScreenOffTransition);
+        pw.println("  mDozeStateOverride=" + mDozeStateOverride);
+
         IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
         if (mDisplayPowerProximityStateController != null) {
             mDisplayPowerProximityStateController.dumpLocal(ipw);
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index c3abfc1..f168f43 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -349,17 +349,17 @@
 
         @Override
         public void onUserStarting(@NonNull TargetUser user) {
-            mLockSettingsService.onStartUser(user.getUserIdentifier());
+            mLockSettingsService.onUserStarting(user.getUserIdentifier());
         }
 
         @Override
         public void onUserUnlocking(@NonNull TargetUser user) {
-            mLockSettingsService.onUnlockUser(user.getUserIdentifier());
+            mLockSettingsService.onUserUnlocking(user.getUserIdentifier());
         }
 
         @Override
         public void onUserStopped(@NonNull TargetUser user) {
-            mLockSettingsService.onCleanupUser(user.getUserIdentifier());
+            mLockSettingsService.onUserStopped(user.getUserIdentifier());
         }
     }
 
@@ -784,7 +784,7 @@
     }
 
     @VisibleForTesting
-    void onCleanupUser(int userId) {
+    void onUserStopped(int userId) {
         hideEncryptionNotification(new UserHandle(userId));
         // User is stopped with its CE key evicted. Restore strong auth requirement to the default
         // flags after boot since stopping and restarting a user later is equivalent to rebooting
@@ -796,7 +796,7 @@
         }
     }
 
-    private void onStartUser(final int userId) {
+    private void onUserStarting(final int userId) {
         maybeShowEncryptionNotificationForUser(userId, "user started");
     }
 
@@ -832,7 +832,7 @@
         }
     }
 
-    private void onUnlockUser(final int userId) {
+    private void onUserUnlocking(final int userId) {
         // Perform tasks which require locks in LSS on a handler, as we are callbacks from
         // ActivityManager.unlockUser()
         mHandler.post(new Runnable() {
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index 4892c22..83a3125 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -22,6 +22,7 @@
 import static android.media.MediaRoute2ProviderService.REASON_UNKNOWN_ERROR;
 import static android.media.MediaRouter2Utils.getOriginalId;
 import static android.media.MediaRouter2Utils.getProviderId;
+
 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
 import static com.android.server.media.MediaFeatureFlagManager.FEATURE_SCANNING_MINIMUM_PACKAGE_IMPORTANCE;
 
@@ -487,12 +488,13 @@
 
         final int callerUid = Binder.getCallingUid();
         final int callerPid = Binder.getCallingPid();
-        final int userId = UserHandle.getUserHandleForUid(callerUid).getIdentifier();
+        final int callerUserId = UserHandle.getUserHandleForUid(callerUid).getIdentifier();
 
         final long token = Binder.clearCallingIdentity();
         try {
             synchronized (mLock) {
-                registerManagerLocked(manager, callerUid, callerPid, callerPackageName, userId);
+                registerManagerLocked(
+                        manager, callerUid, callerPid, callerPackageName, callerUserId);
             }
         } finally {
             Binder.restoreCallingIdentity(token);
@@ -1156,8 +1158,12 @@
     }
 
     @GuardedBy("mLock")
-    private void registerManagerLocked(@NonNull IMediaRouter2Manager manager,
-            int callerUid, int callerPid, @NonNull String callerPackageName, int userId) {
+    private void registerManagerLocked(
+            @NonNull IMediaRouter2Manager manager,
+            int callerUid,
+            int callerPid,
+            @NonNull String callerPackageName,
+            int callerUserId) {
         final IBinder binder = manager.asBinder();
         ManagerRecord managerRecord = mAllManagerRecords.get(binder);
 
@@ -1167,14 +1173,17 @@
             return;
         }
 
-        Slog.i(TAG, TextUtils.formatSimple(
-                "registerManager | callerUid: %d, callerPid: %d, package: %s, user: %d",
-                callerUid, callerPid, callerPackageName, userId));
+        Slog.i(
+                TAG,
+                TextUtils.formatSimple(
+                        "registerManager | callerUid: %d, callerPid: %d, callerPackage: %s,"
+                            + " callerUserId: %d",
+                        callerUid, callerPid, callerPackageName, callerUserId));
 
         mContext.enforcePermission(Manifest.permission.MEDIA_CONTENT_CONTROL, callerPid, callerUid,
                 "Must hold MEDIA_CONTENT_CONTROL permission.");
 
-        UserRecord userRecord = getOrCreateUserRecordLocked(userId);
+        UserRecord userRecord = getOrCreateUserRecordLocked(callerUserId);
         managerRecord = new ManagerRecord(
                 userRecord, manager, callerUid, callerPid, callerPackageName);
         try {
diff --git a/services/core/java/com/android/server/pm/BroadcastHelper.java b/services/core/java/com/android/server/pm/BroadcastHelper.java
index 9a69d77..e367609 100644
--- a/services/core/java/com/android/server/pm/BroadcastHelper.java
+++ b/services/core/java/com/android/server/pm/BroadcastHelper.java
@@ -17,9 +17,14 @@
 package com.android.server.pm;
 
 import static android.os.PowerExemptionManager.REASON_LOCKED_BOOT_COMPLETED;
+import static android.os.PowerExemptionManager.REASON_PACKAGE_REPLACED;
 import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED;
+import static android.os.Process.SYSTEM_UID;
 import static android.safetylabel.SafetyLabelConstants.SAFETY_LABEL_CHANGE_NOTIFICATIONS_ENABLED;
+
+import static com.android.server.pm.PackageManagerService.DEBUG_BACKUP;
 import static com.android.server.pm.PackageManagerService.DEBUG_INSTALL;
+import static com.android.server.pm.PackageManagerService.EMPTY_INT_ARRAY;
 import static com.android.server.pm.PackageManagerService.PACKAGE_SCHEME;
 import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
 import static com.android.server.pm.PackageManagerService.TAG;
@@ -28,6 +33,7 @@
 import android.annotation.AppIdInt;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
 import android.app.BroadcastOptions;
@@ -38,12 +44,18 @@
 import android.content.Intent;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Handler;
 import android.os.PowerExemptionManager;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.os.storage.StorageManager;
+import android.os.storage.VolumeInfo;
 import android.provider.DeviceConfig;
+import android.stats.storage.StorageEnums;
 import android.util.IntArray;
 import android.util.Log;
 import android.util.Pair;
@@ -51,10 +63,15 @@
 import android.util.SparseArray;
 
 import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.FrameworkStatsLog;
+import com.android.server.pm.pkg.AndroidPackage;
+import com.android.server.pm.pkg.PackageStateInternal;
+import com.android.server.pm.pkg.PackageUserStateInternal;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.function.BiFunction;
-import java.util.function.Supplier;
 
 /**
  * Helper class to send broadcasts for various situations.
@@ -70,14 +87,20 @@
     private final UserManagerInternal mUmInternal;
     private final ActivityManagerInternal mAmInternal;
     private final Context mContext;
+    private final Handler mHandler;
+    private final PackageMonitorCallbackHelper mPackageMonitorCallbackHelper;
+    private final AppsFilterSnapshot mAppsFilter;
 
     BroadcastHelper(PackageManagerServiceInjector injector) {
         mUmInternal = injector.getUserManagerInternal();
         mAmInternal = injector.getActivityManagerInternal();
         mContext = injector.getContext();
+        mHandler = injector.getHandler();
+        mPackageMonitorCallbackHelper = injector.getPackageMonitorCallbackHelper();
+        mAppsFilter = injector.getAppsFilter();
     }
 
-    public void sendPackageBroadcast(final String action, final String pkg, final Bundle extras,
+    void sendPackageBroadcast(final String action, final String pkg, final Bundle extras,
             final int flags, final String targetPkg, final IIntentReceiver finishedReceiver,
             final int[] userIds, int[] instantUserIds,
             @Nullable SparseArray<int[]> broadcastAllowList,
@@ -114,9 +137,16 @@
      * the system and applications allowed to see instant applications to receive package
      * lifecycle events for instant applications.
      */
-    public void doSendBroadcast(String action, String pkg, Bundle extras,
-            int flags, String targetPkg, IIntentReceiver finishedReceiver,
-            int[] userIds, boolean isInstantApp, @Nullable SparseArray<int[]> broadcastAllowList,
+    private void doSendBroadcast(
+            @NonNull String action,
+            @Nullable String pkg,
+            @Nullable Bundle extras,
+            int flags,
+            @Nullable String targetPkg,
+            @Nullable IIntentReceiver finishedReceiver,
+            @NonNull int[] userIds,
+            boolean isInstantApp,
+            @Nullable SparseArray<int[]> broadcastAllowList,
             @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver,
             @Nullable Bundle bOptions) {
         for (int userId : userIds) {
@@ -166,9 +196,11 @@
         }
     }
 
-    public void sendResourcesChangedBroadcast(@NonNull Supplier<Computer> snapshotComputer,
-            boolean mediaStatus, boolean replacing, @NonNull String[] pkgNames,
-            @NonNull int[] uids) {
+    void sendResourcesChangedBroadcast(@NonNull Computer snapshot,
+                                       boolean mediaStatus,
+                                       boolean replacing,
+                                       @NonNull String[] pkgNames,
+                                       @NonNull int[] uids) {
         if (ArrayUtils.isEmpty(pkgNames) || ArrayUtils.isEmpty(uids)) {
             return;
         }
@@ -184,7 +216,7 @@
                 null /* targetPkg */, null /* finishedReceiver */, null /* userIds */,
                 null /* instantUserIds */, null /* broadcastAllowList */,
                 (callingUid, intentExtras) -> filterExtrasChangedPackageList(
-                        snapshotComputer.get(), callingUid, intentExtras),
+                        snapshot, callingUid, intentExtras),
                 null /* bOptions */);
     }
 
@@ -193,8 +225,9 @@
      * automatically without needing an explicit launch.
      * Send it a LOCKED_BOOT_COMPLETED/BOOT_COMPLETED if it would ordinarily have gotten ones.
      */
-    public void sendBootCompletedBroadcastToSystemApp(
-            String packageName, boolean includeStopped, int userId) {
+    private void sendBootCompletedBroadcastToSystemApp(@NonNull String packageName,
+                                                       boolean includeStopped,
+                                                       int userId) {
         // If user is not running, the app didn't miss any broadcast
         if (!mUmInternal.isUserRunning(userId)) {
             return;
@@ -229,7 +262,7 @@
         }
     }
 
-    public @NonNull BroadcastOptions getTemporaryAppAllowlistBroadcastOptions(
+    private @NonNull BroadcastOptions getTemporaryAppAllowlistBroadcastOptions(
             @PowerExemptionManager.ReasonCode int reasonCode) {
         long duration = 10_000;
         if (mAmInternal != null) {
@@ -242,9 +275,14 @@
         return bOptions;
     }
 
-    public void sendPackageChangedBroadcast(String packageName, boolean dontKillApp,
-            ArrayList<String> componentNames, int packageUid, String reason,
-            int[] userIds, int[] instantUserIds, SparseArray<int[]> broadcastAllowList) {
+    private void sendPackageChangedBroadcast(@NonNull String packageName,
+                                             boolean dontKillApp,
+                                             @NonNull ArrayList<String> componentNames,
+                                             int packageUid,
+                                             @Nullable String reason,
+                                             @Nullable int[] userIds,
+                                             @Nullable int[] instantUserIds,
+                                             @Nullable SparseArray<int[]> broadcastAllowList) {
         if (DEBUG_INSTALL) {
             Log.v(TAG, "Sending package changed: package=" + packageName + " components="
                     + componentNames);
@@ -269,7 +307,7 @@
                 null /* bOptions */);
     }
 
-    public static void sendDeviceCustomizationReadyBroadcast() {
+    static void sendDeviceCustomizationReadyBroadcast() {
         final Intent intent = new Intent(Intent.ACTION_DEVICE_CUSTOMIZATION_READY);
         intent.setFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
         final IActivityManager am = ActivityManager.getService();
@@ -285,15 +323,23 @@
         }
     }
 
-    public void sendSessionCommitBroadcast(PackageInstaller.SessionInfo sessionInfo, int userId,
-            int launcherUid, @Nullable ComponentName launcherComponent,
-            @Nullable String appPredictionServicePackage) {
+    void sendSessionCommitBroadcast(@NonNull Computer snapshot,
+                                    @NonNull PackageInstaller.SessionInfo sessionInfo,
+                                    int userId,
+                                    @Nullable String appPredictionServicePackage) {
+        UserManagerService ums = UserManagerService.getInstance();
+        if (ums == null || sessionInfo.isStaged()) {
+            return;
+        }
+        final UserInfo parent = ums.getProfileParent(userId);
+        final int launcherUserId = (parent != null) ? parent.id : userId;
+        final ComponentName launcherComponent = snapshot.getDefaultHomeActivity(launcherUserId);
         if (launcherComponent != null) {
             Intent launcherIntent = new Intent(PackageInstaller.ACTION_SESSION_COMMITTED)
                     .putExtra(PackageInstaller.EXTRA_SESSION, sessionInfo)
                     .putExtra(Intent.EXTRA_USER, UserHandle.of(userId))
                     .setPackage(launcherComponent.getPackageName());
-            mContext.sendBroadcastAsUser(launcherIntent, UserHandle.of(launcherUid));
+            mContext.sendBroadcastAsUser(launcherIntent, UserHandle.of(launcherUserId));
         }
         // TODO(b/122900055) Change/Remove this and replace with new permission role.
         if (appPredictionServicePackage != null) {
@@ -301,30 +347,278 @@
                     .putExtra(PackageInstaller.EXTRA_SESSION, sessionInfo)
                     .putExtra(Intent.EXTRA_USER, UserHandle.of(userId))
                     .setPackage(appPredictionServicePackage);
-            mContext.sendBroadcastAsUser(predictorIntent, UserHandle.of(launcherUid));
+            mContext.sendBroadcastAsUser(predictorIntent, UserHandle.of(launcherUserId));
         }
     }
 
-    public void sendPreferredActivityChangedBroadcast(int userId) {
-        final IActivityManager am = ActivityManager.getService();
-        if (am == null) {
+    void sendPreferredActivityChangedBroadcast(int userId) {
+        mHandler.post(() -> {
+            final IActivityManager am = ActivityManager.getService();
+            if (am == null) {
+                return;
+            }
+
+            final Intent intent = new Intent(Intent.ACTION_PREFERRED_ACTIVITY_CHANGED);
+            intent.putExtra(Intent.EXTRA_USER_HANDLE, userId);
+            intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+            try {
+                am.broadcastIntentWithFeature(null, null, intent, null, null,
+                        0, null, null, null, null, null, android.app.AppOpsManager.OP_NONE,
+                        null, false, false, userId);
+            } catch (RemoteException e) {
+            }
+        });
+    }
+
+    void sendPostInstallBroadcasts(@NonNull Computer snapshot,
+                                   @NonNull InstallRequest request,
+                                   @NonNull String packageName,
+                                   @NonNull String requiredPermissionControllerPackage,
+                                   @NonNull String[] requiredVerifierPackages,
+                                   @NonNull String requiredInstallerPackage,
+                                   @NonNull PackageSender packageSender,
+                                   boolean isLaunchedForRestore,
+                                   boolean isKillApp,
+                                   boolean isUpdate,
+                                   boolean isArchived) {
+        // Send the removed broadcasts
+        if (request.getRemovedInfo() != null) {
+            if (request.getRemovedInfo().mIsExternal) {
+                if (DEBUG_INSTALL) {
+                    Slog.i(TAG, "upgrading pkg " + request.getRemovedInfo().mRemovedPackage
+                            + " is ASEC-hosted -> UNAVAILABLE");
+                }
+                final String[] pkgNames = new String[]{
+                        request.getRemovedInfo().mRemovedPackage};
+                final int[] uids = new int[]{request.getRemovedInfo().mUid};
+                notifyResourcesChanged(
+                        false /* mediaStatus */, true /* replacing */, pkgNames, uids);
+                sendResourcesChangedBroadcast(
+                        snapshot, false /* mediaStatus */, true /* replacing */, pkgNames, uids);
+            }
+            sendPackageRemovedBroadcasts(
+                    request.getRemovedInfo(), packageSender, isKillApp, false /*removedBySystem*/,
+                    false /*isArchived*/);
+        }
+
+        final int[] firstUserIds = request.getFirstTimeBroadcastUserIds();
+        final int[] firstInstantUserIds = request.getFirstTimeBroadcastInstantUserIds();
+        final int[] updateUserIds = request.getUpdateBroadcastUserIds();
+        final int[] instantUserIds = request.getUpdateBroadcastInstantUserIds();
+
+        final String installerPackageName =
+                request.getInstallerPackageName() != null
+                        ? request.getInstallerPackageName()
+                        : request.getRemovedInfo() != null
+                        ? request.getRemovedInfo().mInstallerPackageName
+                        : null;
+
+        Bundle extras = new Bundle();
+        extras.putInt(Intent.EXTRA_UID, request.getAppId());
+        if (isUpdate) {
+            extras.putBoolean(Intent.EXTRA_REPLACING, true);
+        }
+        if (isArchived) {
+            extras.putBoolean(Intent.EXTRA_ARCHIVAL, true);
+        }
+        extras.putInt(PackageInstaller.EXTRA_DATA_LOADER_TYPE, request.getDataLoaderType());
+
+        final String staticSharedLibraryName = request.getPkg().getStaticSharedLibraryName();
+        // If a package is a static shared library, then only the installer of the package
+        // should get the broadcast.
+        if (installerPackageName != null && staticSharedLibraryName != null) {
+            sendPackageBroadcastAndNotify(Intent.ACTION_PACKAGE_ADDED, packageName,
+                    extras, 0 /*flags*/,
+                    installerPackageName, null /*finishedReceiver*/,
+                    request.getNewUsers(), null /* instantUserIds*/,
+                    null /* broadcastAllowList */, null);
+        }
+
+        // Send installed broadcasts if the package is not a static shared lib.
+        if (staticSharedLibraryName == null) {
+            // Send PACKAGE_ADDED broadcast for users that see the package for the first time
+            // sendPackageAddedForNewUsers also deals with system apps
+            final int appId = UserHandle.getAppId(request.getAppId());
+            final boolean isSystem = request.isInstallSystem();
+            final boolean isVirtualPreload =
+                    ((request.getInstallFlags() & PackageManager.INSTALL_VIRTUAL_PRELOAD) != 0);
+            sendPackageAddedForNewUsers(snapshot, packageName,
+                    isSystem || isVirtualPreload,
+                    isVirtualPreload /*startReceiver*/, appId,
+                    firstUserIds, firstInstantUserIds, isArchived, request.getDataLoaderType());
+
+            // Send PACKAGE_ADDED broadcast for users that don't see
+            // the package for the first time
+
+            // Send to all running apps.
+            final SparseArray<int[]> newBroadcastAllowList =
+                    mAppsFilter.getVisibilityAllowList(snapshot,
+                            snapshot.getPackageStateInternal(packageName, Process.SYSTEM_UID),
+                            updateUserIds, snapshot.getPackageStates());
+            sendPackageBroadcastAndNotify(Intent.ACTION_PACKAGE_ADDED, packageName,
+                    extras, 0 /*flags*/,
+                    null /*targetPackage*/, null /*finishedReceiver*/,
+                    updateUserIds, instantUserIds, newBroadcastAllowList, null);
+            // Send to the installer, even if it's not running.
+            if (installerPackageName != null) {
+                sendPackageBroadcastAndNotify(Intent.ACTION_PACKAGE_ADDED, packageName,
+                        extras, 0 /*flags*/,
+                        installerPackageName, null /*finishedReceiver*/,
+                        updateUserIds, instantUserIds, null /* broadcastAllowList */, null);
+            }
+            // Send to PermissionController for all update users, even if it may not be running
+            // for some users
+            if (isPrivacySafetyLabelChangeNotificationsEnabled(mContext)) {
+                sendPackageBroadcastAndNotify(Intent.ACTION_PACKAGE_ADDED, packageName,
+                        extras, 0 /*flags*/,
+                        requiredPermissionControllerPackage, null /*finishedReceiver*/,
+                        updateUserIds, instantUserIds, null /* broadcastAllowList */, null);
+            }
+            // Notify required verifier(s) that are not the installer of record for the package.
+            for (String verifierPackageName : requiredVerifierPackages) {
+                if (verifierPackageName != null && !verifierPackageName.equals(
+                        installerPackageName)) {
+                    sendPackageBroadcastAndNotify(Intent.ACTION_PACKAGE_ADDED,
+                            packageName,
+                            extras, 0 /*flags*/,
+                            verifierPackageName, null /*finishedReceiver*/,
+                            updateUserIds, instantUserIds, null /* broadcastAllowList */,
+                            null);
+                }
+            }
+            // If package installer is defined, notify package installer about new
+            // app installed
+            sendPackageBroadcastAndNotify(Intent.ACTION_PACKAGE_ADDED, packageName,
+                    extras, Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND /*flags*/,
+                    requiredInstallerPackage, null /*finishedReceiver*/,
+                    firstUserIds, instantUserIds, null /* broadcastAllowList */, null);
+
+            // Send replaced for users that don't see the package for the first time
+            if (isUpdate) {
+                sendPackageBroadcastAndNotify(Intent.ACTION_PACKAGE_REPLACED,
+                        packageName, extras, 0 /*flags*/,
+                        null /*targetPackage*/, null /*finishedReceiver*/,
+                        updateUserIds, instantUserIds,
+                        request.getRemovedInfo().mBroadcastAllowList, null);
+                if (installerPackageName != null) {
+                    sendPackageBroadcastAndNotify(Intent.ACTION_PACKAGE_REPLACED, packageName,
+                            extras, 0 /*flags*/,
+                            installerPackageName, null /*finishedReceiver*/,
+                            updateUserIds, instantUserIds, null /*broadcastAllowList*/,
+                            null);
+                }
+                for (String verifierPackageName : requiredVerifierPackages) {
+                    if (verifierPackageName != null && !verifierPackageName.equals(
+                            installerPackageName)) {
+                        sendPackageBroadcastAndNotify(Intent.ACTION_PACKAGE_REPLACED,
+                                packageName, extras, 0 /*flags*/, verifierPackageName,
+                                null /*finishedReceiver*/, updateUserIds, instantUserIds,
+                                null /*broadcastAllowList*/, null);
+                    }
+                }
+                sendPackageBroadcastAndNotify(Intent.ACTION_MY_PACKAGE_REPLACED,
+                        null /*package*/, null /*extras*/, 0 /*flags*/,
+                        packageName /*targetPackage*/,
+                        null /*finishedReceiver*/, updateUserIds, instantUserIds,
+                        null /*broadcastAllowList*/,
+                        getTemporaryAppAllowlistBroadcastOptions(
+                                REASON_PACKAGE_REPLACED).toBundle());
+            } else if (isLaunchedForRestore && !request.isInstallSystem()) {
+                // First-install and we did a restore, so we're responsible for the
+                // first-launch broadcast.
+                if (DEBUG_BACKUP) {
+                    Slog.i(TAG, "Post-restore of " + packageName
+                            + " sending FIRST_LAUNCH in " + Arrays.toString(firstUserIds));
+                }
+                sendFirstLaunchBroadcast(packageName, installerPackageName,
+                        firstUserIds, firstInstantUserIds);
+            }
+
+            // Send broadcast package appeared if external for all users
+            if (request.getPkg().isExternalStorage()) {
+                if (!isUpdate) {
+                    final StorageManager storage = mContext.getSystemService(StorageManager.class);
+                    VolumeInfo volume =
+                            storage.findVolumeByUuid(
+                                    StorageManager.convert(
+                                            request.getPkg().getVolumeUuid()).toString());
+                    int packageExternalStorageType =
+                            PackageManagerServiceUtils.getPackageExternalStorageType(volume,
+                                    /* isExternalStorage */ true);
+                    // If the package was installed externally, log it.
+                    if (packageExternalStorageType != StorageEnums.UNKNOWN) {
+                        FrameworkStatsLog.write(
+                                FrameworkStatsLog.APP_INSTALL_ON_EXTERNAL_STORAGE_REPORTED,
+                                packageExternalStorageType, packageName);
+                    }
+                }
+                if (DEBUG_INSTALL) {
+                    Slog.i(TAG, "upgrading pkg " + packageName + " is external");
+                }
+                if (!isArchived) {
+                    final String[] pkgNames = new String[]{packageName};
+                    final int[] uids = new int[]{request.getPkg().getUid()};
+                    sendResourcesChangedBroadcast(snapshot,
+                            true /* mediaStatus */, true /* replacing */, pkgNames, uids);
+                    notifyResourcesChanged(true /* mediaStatus */,
+                            true /* replacing */, pkgNames, uids);
+                }
+            }
+        } else { // if static shared lib
+            final ArrayList<AndroidPackage> libraryConsumers = request.getLibraryConsumers();
+            if (!ArrayUtils.isEmpty(libraryConsumers)) {
+                // No need to kill consumers if it's installation of new version static shared lib.
+                final boolean dontKillApp = !isUpdate;
+                for (int i = 0; i < libraryConsumers.size(); i++) {
+                    AndroidPackage pkg = libraryConsumers.get(i);
+                    // send broadcast that all consumers of the static shared library have changed
+                    sendPackageChangedBroadcast(snapshot, pkg.getPackageName(),
+                            dontKillApp,
+                            new ArrayList<>(Collections.singletonList(pkg.getPackageName())),
+                            pkg.getUid(), null);
+                }
+            }
+        }
+    }
+
+    private void sendPackageAddedForNewUsers(@NonNull Computer snapshot,
+                                             @NonNull String packageName,
+                                             boolean sendBootCompleted,
+                                             boolean includeStopped,
+                                             @AppIdInt int appId,
+                                             int[] userIds,
+                                             int[] instantUserIds,
+                                             boolean isArchived,
+                                             int dataLoaderType) {
+        if (ArrayUtils.isEmpty(userIds) && ArrayUtils.isEmpty(instantUserIds)) {
             return;
         }
-
-        final Intent intent = new Intent(Intent.ACTION_PREFERRED_ACTIVITY_CHANGED);
-        intent.putExtra(Intent.EXTRA_USER_HANDLE, userId);
-        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
-        try {
-            am.broadcastIntentWithFeature(null, null, intent, null, null,
-                    0, null, null, null, null, null, android.app.AppOpsManager.OP_NONE,
-                    null, false, false, userId);
-        } catch (RemoteException e) {
+        SparseArray<int[]> broadcastAllowList = mAppsFilter.getVisibilityAllowList(snapshot,
+                snapshot.getPackageStateInternal(packageName, Process.SYSTEM_UID),
+                userIds, snapshot.getPackageStates());
+        mHandler.post(
+                () -> sendPackageAddedForNewUsers(packageName, appId, userIds,
+                        instantUserIds, isArchived, dataLoaderType, broadcastAllowList));
+        mPackageMonitorCallbackHelper.notifyPackageAddedForNewUsers(packageName, appId, userIds,
+                instantUserIds, isArchived, dataLoaderType, broadcastAllowList, mHandler);
+        if (sendBootCompleted && !ArrayUtils.isEmpty(userIds)) {
+            mHandler.post(() -> {
+                        for (int userId : userIds) {
+                            sendBootCompletedBroadcastToSystemApp(
+                                    packageName, includeStopped, userId);
+                        }
+                    }
+            );
         }
     }
 
-    public void sendPackageAddedForNewUsers(String packageName, @AppIdInt int appId, int[] userIds,
-            int[] instantUserIds, boolean isArchived, int dataLoaderType,
-            SparseArray<int[]> broadcastAllowlist) {
+    private void sendPackageAddedForNewUsers(@NonNull String packageName,
+                                             @AppIdInt int appId,
+                                             int[] userIds,
+                                             int[] instantUserIds,
+                                             boolean isArchived,
+                                             int dataLoaderType,
+                                             @NonNull SparseArray<int[]> broadcastAllowlist) {
         Bundle extras = new Bundle(1);
         // Set to UID of the first user, EXTRA_UID is automatically updated in sendPackageBroadcast
         final int uid = UserHandle.getUid(
@@ -349,7 +643,30 @@
         }
     }
 
-    public void sendFirstLaunchBroadcast(String pkgName, String installerPkg,
+    void sendPackageAddedForUser(@NonNull Computer snapshot,
+                                 @NonNull String packageName,
+                                 @NonNull PackageStateInternal packageState,
+                                 int userId,
+                                 boolean isArchived,
+                                 int dataLoaderType,
+                                 @Nullable String appPredictionServicePackage) {
+        final PackageUserStateInternal userState = packageState.getUserStateOrDefault(userId);
+        final boolean isSystem = packageState.isSystem();
+        final boolean isInstantApp = userState.isInstantApp();
+        final int[] userIds = isInstantApp ? EMPTY_INT_ARRAY : new int[] { userId };
+        final int[] instantUserIds = isInstantApp ? new int[] { userId } : EMPTY_INT_ARRAY;
+        sendPackageAddedForNewUsers(snapshot, packageName, isSystem /*sendBootCompleted*/,
+                false /*startReceiver*/, packageState.getAppId(), userIds, instantUserIds,
+                isArchived, dataLoaderType);
+
+        // Send a session commit broadcast
+        final PackageInstaller.SessionInfo info = new PackageInstaller.SessionInfo();
+        info.installReason = userState.getInstallReason();
+        info.appPackageName = packageName;
+        sendSessionCommitBroadcast(snapshot, info, userId, appPredictionServicePackage);
+    }
+
+    void sendFirstLaunchBroadcast(String pkgName, String installerPkg,
             int[] userIds, int[] instantUserIds) {
         sendPackageBroadcast(Intent.ACTION_PACKAGE_FIRST_LAUNCH, pkgName, null, 0,
                 installerPkg, null, userIds, instantUserIds, null /* broadcastAllowList */,
@@ -366,7 +683,7 @@
      * access all the packages in the extras.
      */
     @Nullable
-    public static Bundle filterExtrasChangedPackageList(@NonNull Computer snapshot, int callingUid,
+    private static Bundle filterExtrasChangedPackageList(@NonNull Computer snapshot, int callingUid,
             @NonNull Bundle extras) {
         if (UserHandle.isCore(callingUid)) {
             // see all
@@ -392,7 +709,7 @@
     }
 
     /** Returns whether the Safety Label Change notification, a privacy feature, is enabled. */
-    public static boolean isPrivacySafetyLabelChangeNotificationsEnabled(Context context) {
+    private static boolean isPrivacySafetyLabelChangeNotificationsEnabled(Context context) {
         PackageManager packageManager = context.getPackageManager();
         return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
                 SAFETY_LABEL_CHANGE_NOTIFICATIONS_ENABLED, true)
@@ -424,4 +741,323 @@
                 pkgList.size() > 0 ? pkgList.toArray(new String[pkgList.size()]) : null,
                 uidList != null && uidList.size() > 0 ? uidList.toArray() : null);
     }
+
+    void sendApplicationHiddenForUser(@NonNull String packageName,
+                                      @NonNull PackageStateInternal packageState,
+                                      int userId,
+                                      @NonNull PackageSender packageSender) {
+        final PackageRemovedInfo info = new PackageRemovedInfo();
+        info.mRemovedPackage = packageName;
+        info.mInstallerPackageName = packageState.getInstallSource().mInstallerPackageName;
+        info.mRemovedUsers = new int[] {userId};
+        info.mBroadcastUsers = new int[] {userId};
+        info.mUid = UserHandle.getUid(userId, packageState.getAppId());
+        info.mRemovedPackageVersionCode = packageState.getVersionCode();
+        sendPackageRemovedBroadcasts(info, packageSender, true /*killApp*/,
+                false /*removedBySystem*/, false /*isArchived*/);
+    }
+
+    void sendPackageChangedBroadcast(@NonNull Computer snapshot,
+                                     @NonNull String packageName,
+                                     boolean dontKillApp,
+                                     @NonNull ArrayList<String> componentNames,
+                                     int packageUid,
+                                     @NonNull String reason) {
+        PackageStateInternal setting = snapshot.getPackageStateInternal(packageName,
+                Process.SYSTEM_UID);
+        if (setting == null) {
+            return;
+        }
+        final int userId = UserHandle.getUserId(packageUid);
+        final boolean isInstantApp =
+                snapshot.isInstantAppInternal(packageName, userId, Process.SYSTEM_UID);
+        final int[] userIds = isInstantApp ? EMPTY_INT_ARRAY : new int[] { userId };
+        final int[] instantUserIds = isInstantApp ? new int[] { userId } : EMPTY_INT_ARRAY;
+        final SparseArray<int[]> broadcastAllowList =
+                isInstantApp ? null : snapshot.getVisibilityAllowLists(packageName, userIds);
+        mHandler.post(() -> sendPackageChangedBroadcast(
+                packageName, dontKillApp, componentNames, packageUid, reason, userIds,
+                instantUserIds, broadcastAllowList));
+        mPackageMonitorCallbackHelper.notifyPackageChanged(packageName, dontKillApp, componentNames,
+                packageUid, reason, userIds, instantUserIds, broadcastAllowList, mHandler);
+    }
+
+    private void sendPackageBroadcastAndNotify(@NonNull String action,
+                                               @NonNull  String pkg,
+                                               @NonNull  Bundle extras,
+                                               int flags,
+                                               @Nullable String targetPkg,
+                                               @Nullable IIntentReceiver finishedReceiver,
+                                               @NonNull int[] userIds,
+                                               @NonNull int[] instantUserIds,
+                                               @Nullable SparseArray<int[]> broadcastAllowList,
+                                               @Nullable Bundle bOptions) {
+        mHandler.post(() -> sendPackageBroadcast(action, pkg, extras, flags,
+                targetPkg, finishedReceiver, userIds, instantUserIds, broadcastAllowList,
+                null /* filterExtrasForReceiver */, bOptions));
+        if (targetPkg == null) {
+            // For some broadcast action, e.g. ACTION_PACKAGE_ADDED, this method will be called
+            // many times to different targets, e.g. installer app, permission controller, other
+            // registered apps. We should filter it to avoid calling back many times for the same
+            // action. When the targetPkg is set, it sends the broadcast to specific app, e.g.
+            // installer app or null for registered apps. The callback only need to send back to the
+            // registered apps so we check the null condition here.
+            notifyPackageMonitor(action, pkg, extras, userIds, instantUserIds, broadcastAllowList);
+        }
+    }
+
+    void sendSystemPackageUpdatedBroadcasts(@NonNull PackageRemovedInfo packageRemovedInfo) {
+        if (!packageRemovedInfo.mIsRemovedPackageSystemUpdate) {
+            return;
+        }
+
+        final String removedPackage = packageRemovedInfo.mRemovedPackage;
+        final int removedAppId = packageRemovedInfo.mRemovedAppId;
+        final int uid = packageRemovedInfo.mUid;
+        final String installerPackageName = packageRemovedInfo.mInstallerPackageName;
+        final SparseArray<int[]> broadcastAllowList = packageRemovedInfo.mBroadcastAllowList;
+
+        Bundle extras = new Bundle(2);
+        extras.putInt(Intent.EXTRA_UID, removedAppId >= 0 ? removedAppId : uid);
+        extras.putBoolean(Intent.EXTRA_REPLACING, true);
+        sendPackageBroadcastAndNotify(Intent.ACTION_PACKAGE_ADDED, removedPackage, extras,
+                0, null /*targetPackage*/, null, null, null, broadcastAllowList, null);
+
+        if (installerPackageName != null) {
+            sendPackageBroadcastAndNotify(Intent.ACTION_PACKAGE_ADDED,
+                    removedPackage, extras, 0 /*flags*/,
+                    installerPackageName, null, null, null, null /* broadcastAllowList */,
+                    null);
+            sendPackageBroadcastAndNotify(Intent.ACTION_PACKAGE_REPLACED,
+                    removedPackage, extras, 0 /*flags*/,
+                    installerPackageName, null, null, null, null /* broadcastAllowList */,
+                    null);
+        }
+        sendPackageBroadcastAndNotify(Intent.ACTION_PACKAGE_REPLACED, removedPackage,
+                extras, 0, null /*targetPackage*/, null, null, null, broadcastAllowList, null);
+        sendPackageBroadcastAndNotify(Intent.ACTION_MY_PACKAGE_REPLACED, null, null, 0,
+                removedPackage, null, null, null, null /* broadcastAllowList */,
+                getTemporaryBroadcastOptionsForSystemPackageUpdate(REASON_PACKAGE_REPLACED)
+                        .toBundle());
+    }
+
+    @SuppressLint("AndroidFrameworkRequiresPermission")
+    private @NonNull BroadcastOptions getTemporaryBroadcastOptionsForSystemPackageUpdate(
+            @PowerExemptionManager.ReasonCode int reasonCode) {
+        long duration = 10_000;
+        if (mAmInternal != null) {
+            duration = mAmInternal.getBootTimeTempAllowListDuration();
+        }
+        final BroadcastOptions bOptions = BroadcastOptions.makeBasic();
+        bOptions.setTemporaryAppAllowlist(duration,
+                TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
+                reasonCode, "");
+        return bOptions;
+    }
+
+
+    void sendPackageRemovedBroadcasts(
+            @NonNull PackageRemovedInfo packageRemovedInfo,
+            @NonNull PackageSender packageSender,
+            boolean killApp,
+            boolean removedBySystem,
+            boolean isArchived) {
+        final String removedPackage = packageRemovedInfo.mRemovedPackage;
+        final int removedAppId = packageRemovedInfo.mRemovedAppId;
+        final int uid = packageRemovedInfo.mUid;
+        final String installerPackageName = packageRemovedInfo.mInstallerPackageName;
+        final int[] broadcastUserIds = packageRemovedInfo.mBroadcastUsers;
+        final int[] instantUserIds = packageRemovedInfo.mInstantUserIds;
+        final SparseArray<int[]> broadcastAllowList = packageRemovedInfo.mBroadcastAllowList;
+        final boolean dataRemoved = packageRemovedInfo.mDataRemoved;
+        final boolean isUpdate = packageRemovedInfo.mIsUpdate;
+        final boolean isRemovedPackageSystemUpdate =
+                packageRemovedInfo.mIsRemovedPackageSystemUpdate;
+        final boolean isRemovedForAllUsers = packageRemovedInfo.mRemovedForAllUsers;
+        final boolean isStaticSharedLib = packageRemovedInfo.mIsStaticSharedLib;
+
+        Bundle extras = new Bundle();
+        final int removedUid = removedAppId >= 0  ? removedAppId : uid;
+        extras.putInt(Intent.EXTRA_UID, removedUid);
+        extras.putBoolean(Intent.EXTRA_DATA_REMOVED, dataRemoved);
+        extras.putBoolean(Intent.EXTRA_SYSTEM_UPDATE_UNINSTALL, isRemovedPackageSystemUpdate);
+        extras.putBoolean(Intent.EXTRA_DONT_KILL_APP, !killApp);
+        extras.putBoolean(Intent.EXTRA_USER_INITIATED, !removedBySystem);
+        final boolean isReplace = isUpdate || isRemovedPackageSystemUpdate;
+        if (isReplace || isArchived) {
+            extras.putBoolean(Intent.EXTRA_REPLACING, true);
+        }
+        if (isArchived) {
+            extras.putBoolean(Intent.EXTRA_ARCHIVAL, true);
+        }
+        extras.putBoolean(Intent.EXTRA_REMOVED_FOR_ALL_USERS, isRemovedForAllUsers);
+
+        // Send PACKAGE_REMOVED broadcast to the respective installer.
+        if (removedPackage != null && installerPackageName != null) {
+            sendPackageBroadcastAndNotify(Intent.ACTION_PACKAGE_REMOVED,
+                    removedPackage, extras, 0 /*flags*/,
+                    installerPackageName, null, broadcastUserIds, instantUserIds, null, null);
+        }
+        if (isStaticSharedLib) {
+            // When uninstalling static shared libraries, only the package's installer needs to be
+            // sent a PACKAGE_REMOVED broadcast. There are no other intended recipients.
+            return;
+        }
+        if (removedPackage != null) {
+            sendPackageBroadcastAndNotify(Intent.ACTION_PACKAGE_REMOVED,
+                    removedPackage, extras, 0, null /*targetPackage*/, null,
+                    broadcastUserIds, instantUserIds, broadcastAllowList, null);
+            sendPackageBroadcastAndNotify(Intent.ACTION_PACKAGE_REMOVED_INTERNAL,
+                    removedPackage, extras, 0 /*flags*/, PLATFORM_PACKAGE_NAME,
+                    null /*finishedReceiver*/, broadcastUserIds, instantUserIds,
+                    broadcastAllowList, null /*bOptions*/);
+            if (dataRemoved && !isRemovedPackageSystemUpdate) {
+                sendPackageBroadcastAndNotify(Intent.ACTION_PACKAGE_FULLY_REMOVED,
+                        removedPackage, extras, Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND, null,
+                        null, broadcastUserIds, instantUserIds, broadcastAllowList, null);
+                packageSender.notifyPackageRemoved(removedPackage, removedUid);
+            }
+        }
+        if (removedAppId >= 0) {
+            // If a system app's updates are uninstalled the UID is not actually removed. Some
+            // services need to know the package name affected.
+            if (isReplace) {
+                extras.putString(Intent.EXTRA_PACKAGE_NAME, removedPackage);
+            }
+
+            sendPackageBroadcastAndNotify(Intent.ACTION_UID_REMOVED,
+                    null, extras, Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND,
+                    null, null, broadcastUserIds, instantUserIds, broadcastAllowList, null);
+        }
+    }
+
+    /**
+     * Send broadcast intents for packages suspension changes.
+     *
+     * @param intent The action name of the suspension intent.
+     * @param pkgList The names of packages which have suspension changes.
+     * @param uidList The uids of packages which have suspension changes.
+     * @param userId The user where packages reside.
+     */
+    void sendPackagesSuspendedOrUnsuspendedForUser(@NonNull Computer snapshot,
+                                                   @NonNull String intent,
+                                                   @NonNull String[] pkgList,
+                                                   @NonNull int[] uidList,
+                                                   boolean quarantined,
+                                                   int userId) {
+        final Bundle extras = new Bundle(3);
+        extras.putStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST, pkgList);
+        extras.putIntArray(Intent.EXTRA_CHANGED_UID_LIST, uidList);
+        if (quarantined) {
+            extras.putBoolean(Intent.EXTRA_QUARANTINED, true);
+        }
+        final int flags = Intent.FLAG_RECEIVER_REGISTERED_ONLY | Intent.FLAG_RECEIVER_FOREGROUND;
+        final Bundle options = new BroadcastOptions()
+                .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE)
+                .toBundle();
+        mHandler.post(() -> sendPackageBroadcast(intent, null /* pkg */,
+                extras, flags, null /* targetPkg */, null /* finishedReceiver */,
+                new int[]{userId}, null /* instantUserIds */, null /* broadcastAllowList */,
+                (callingUid, intentExtras) -> BroadcastHelper.filterExtrasChangedPackageList(
+                        snapshot, callingUid, intentExtras),
+                options));
+        notifyPackageMonitor(intent, null /* pkg */, extras, new int[]{userId},
+                null /* instantUserIds */, null /* broadcastAllowList */);
+    }
+
+    void sendMyPackageSuspendedOrUnsuspended(@NonNull Computer snapshot,
+                                             @NonNull String[] affectedPackages,
+                                             boolean suspended,
+                                             int userId) {
+        final String action = suspended
+                ? Intent.ACTION_MY_PACKAGE_SUSPENDED
+                : Intent.ACTION_MY_PACKAGE_UNSUSPENDED;
+        mHandler.post(() -> {
+            final IActivityManager am = ActivityManager.getService();
+            if (am == null) {
+                Slog.wtf(TAG, "IActivityManager null. Cannot send MY_PACKAGE_ "
+                        + (suspended ? "" : "UN") + "SUSPENDED broadcasts");
+                return;
+            }
+            final int[] targetUserIds = new int[] {userId};
+            for (String packageName : affectedPackages) {
+                final Bundle appExtras = suspended
+                        ? SuspendPackageHelper.getSuspendedPackageAppExtras(
+                                snapshot, packageName, userId, SYSTEM_UID)
+                        : null;
+                final Bundle intentExtras;
+                if (appExtras != null) {
+                    intentExtras = new Bundle(1);
+                    intentExtras.putBundle(Intent.EXTRA_SUSPENDED_PACKAGE_EXTRAS, appExtras);
+                } else {
+                    intentExtras = null;
+                }
+                doSendBroadcast(action, null, intentExtras,
+                        Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND, packageName, null,
+                        targetUserIds, false, null, null, null);
+            }
+        });
+    }
+
+    /**
+     * Send broadcast intents for packages distracting changes.
+     *
+     * @param pkgList The names of packages which have suspension changes.
+     * @param uidList The uids of packages which have suspension changes.
+     * @param userId The user where packages reside.
+     */
+    void sendDistractingPackagesChanged(@NonNull Computer snapshot,
+                                        @NonNull String[] pkgList,
+                                        @NonNull int[] uidList,
+                                        int userId,
+                                        int distractionFlags) {
+        final Bundle extras = new Bundle();
+        extras.putStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST, pkgList);
+        extras.putIntArray(Intent.EXTRA_CHANGED_UID_LIST, uidList);
+        extras.putInt(Intent.EXTRA_DISTRACTION_RESTRICTIONS, distractionFlags);
+
+        mHandler.post(() -> sendPackageBroadcast(
+                Intent.ACTION_DISTRACTING_PACKAGES_CHANGED, null /* pkg */,
+                extras, Intent.FLAG_RECEIVER_REGISTERED_ONLY, null /* targetPkg */,
+                null /* finishedReceiver */, new int[]{userId}, null /* instantUserIds */,
+                null /* broadcastAllowList */,
+                (callingUid, intentExtras) -> filterExtrasChangedPackageList(
+                        snapshot, callingUid, intentExtras),
+                null /* bOptions */));
+    }
+
+    void sendResourcesChangedBroadcastAndNotify(@NonNull Computer snapshot,
+                                                boolean mediaStatus,
+                                                boolean replacing,
+                                                @NonNull ArrayList<AndroidPackage> packages) {
+        final int size = packages.size();
+        final String[] packageNames = new String[size];
+        final int[] packageUids = new int[size];
+        for (int i = 0; i < size; i++) {
+            final AndroidPackage pkg = packages.get(i);
+            packageNames[i] = pkg.getPackageName();
+            packageUids[i] = pkg.getUid();
+        }
+        sendResourcesChangedBroadcast(snapshot, mediaStatus,
+                replacing, packageNames, packageUids);
+        notifyResourcesChanged(mediaStatus, replacing, packageNames, packageUids);
+    }
+
+    private void notifyPackageMonitor(@NonNull String action,
+                                      @NonNull String pkg,
+                                      @Nullable Bundle extras,
+                                      @NonNull int[] userIds,
+                                      @NonNull int[] instantUserIds,
+                                      @Nullable SparseArray<int[]> broadcastAllowList) {
+        mPackageMonitorCallbackHelper.notifyPackageMonitor(action, pkg, extras, userIds,
+                instantUserIds, broadcastAllowList, mHandler);
+    }
+
+    private void notifyResourcesChanged(boolean mediaStatus,
+                                boolean replacing,
+                                @NonNull String[] pkgNames,
+                                @NonNull int[] uids) {
+        mPackageMonitorCallbackHelper.notifyResourcesChanged(mediaStatus, replacing, pkgNames,
+                uids, mHandler);
+    }
 }
diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java
index 83f90a1..8e767e7 100644
--- a/services/core/java/com/android/server/pm/DeletePackageHelper.java
+++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java
@@ -63,7 +63,6 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.Preconditions;
-import com.android.server.pm.permission.PermissionManagerServiceInternal;
 import com.android.server.pm.pkg.AndroidPackage;
 import com.android.server.pm.pkg.ArchiveState;
 import com.android.server.pm.pkg.PackageStateInternal;
@@ -87,19 +86,16 @@
 
     private final PackageManagerService mPm;
     private final UserManagerInternal mUserManagerInternal;
-    private final PermissionManagerServiceInternal mPermissionManager;
     private final RemovePackageHelper mRemovePackageHelper;
+    private final BroadcastHelper mBroadcastHelper;
 
     // TODO(b/198166813): remove PMS dependency
-    DeletePackageHelper(PackageManagerService pm, RemovePackageHelper removePackageHelper) {
+    DeletePackageHelper(PackageManagerService pm, RemovePackageHelper removePackageHelper,
+                        BroadcastHelper broadcastHelper) {
         mPm = pm;
         mUserManagerInternal = mPm.mInjector.getUserManagerInternal();
-        mPermissionManager = mPm.mInjector.getPermissionManagerServiceInternal();
         mRemovePackageHelper = removePackageHelper;
-    }
-
-    DeletePackageHelper(PackageManagerService pm) {
-        this(pm, new RemovePackageHelper(pm));
+        mBroadcastHelper = broadcastHelper;
     }
 
     /**
@@ -121,7 +117,7 @@
      */
     public int deletePackageX(String packageName, long versionCode, int userId, int deleteFlags,
             boolean removedBySystem) {
-        final PackageRemovedInfo info = new PackageRemovedInfo(mPm);
+        final PackageRemovedInfo info = new PackageRemovedInfo();
         final boolean res;
 
         final int removeUser = (deleteFlags & PackageManager.DELETE_ALL_USERS) != 0
@@ -251,8 +247,9 @@
         if (res) {
             final boolean killApp = (deleteFlags & PackageManager.DELETE_DONT_KILL_APP) == 0;
             final boolean isArchived = (deleteFlags & PackageManager.DELETE_ARCHIVE) != 0;
-            info.sendPackageRemovedBroadcasts(killApp, removedBySystem, isArchived);
-            info.sendSystemPackageUpdatedBroadcasts();
+            mBroadcastHelper.sendPackageRemovedBroadcasts(info, mPm, killApp,
+                    removedBySystem, isArchived);
+            mBroadcastHelper.sendSystemPackageUpdatedBroadcasts(info);
             PackageMetrics.onUninstallSucceeded(info, deleteFlags, removeUser);
         }
 
@@ -314,7 +311,7 @@
                             Slog.i(TAG, "Enabling system stub after removal; pkg: "
                                     + stubPkg.getPackageName());
                         }
-                        new InstallPackageHelper(mPm).enableCompressedPackage(stubPkg, stubPs);
+                        mPm.enableCompressedPackage(stubPkg, stubPs);
                     } else if (DEBUG_COMPRESSION) {
                         Slog.i(TAG, "System stub disabled for all users, leaving uncompressed "
                                 + "after removal; pkg: " + stubPkg.getPackageName());
@@ -491,8 +488,7 @@
             // When an updated system application is deleted we delete the existing resources
             // as well and fall back to existing code in system partition
             deleteInstalledSystemPackage(action, allUserHandles, writeSettings);
-            new InstallPackageHelper(mPm).restoreDisabledSystemPackageLIF(
-                    action, allUserHandles, writeSettings);
+            mPm.restoreDisabledSystemPackageLIF(action, allUserHandles, writeSettings);
         } else {
             if (DEBUG_REMOVE) Slog.d(TAG, "Removing non-system package: " + ps.getPackageName());
             if (ps.isIncremental()) {
diff --git a/services/core/java/com/android/server/pm/DistractingPackageHelper.java b/services/core/java/com/android/server/pm/DistractingPackageHelper.java
index 8ebb6ea..c5ec73b 100644
--- a/services/core/java/com/android/server/pm/DistractingPackageHelper.java
+++ b/services/core/java/com/android/server/pm/DistractingPackageHelper.java
@@ -19,10 +19,7 @@
 import static android.content.pm.PackageManager.RESTRICTION_NONE;
 
 import android.annotation.NonNull;
-import android.content.Intent;
 import android.content.pm.PackageManager.DistractionRestriction;
-import android.os.Bundle;
-import android.os.Handler;
 import android.os.UserHandle;
 import android.util.ArraySet;
 import android.util.IntArray;
@@ -42,17 +39,16 @@
 
     // TODO(b/198166813): remove PMS dependency
     private final PackageManagerService mPm;
-    private final PackageManagerServiceInjector mInjector;
     private final BroadcastHelper mBroadcastHelper;
     private final SuspendPackageHelper mSuspendPackageHelper;
 
     /**
      * Constructor for {@link PackageManagerService}.
      */
-    DistractingPackageHelper(PackageManagerService pm, PackageManagerServiceInjector injector,
-            BroadcastHelper broadcastHelper, SuspendPackageHelper suspendPackageHelper) {
+    DistractingPackageHelper(PackageManagerService pm,
+                             BroadcastHelper broadcastHelper,
+                             SuspendPackageHelper suspendPackageHelper) {
         mPm = pm;
-        mInjector = injector;
         mBroadcastHelper = broadcastHelper;
         mSuspendPackageHelper = suspendPackageHelper;
     }
@@ -127,8 +123,8 @@
         if (!changedPackagesList.isEmpty()) {
             final String[] changedPackages = changedPackagesList.toArray(
                     new String[changedPackagesList.size()]);
-            sendDistractingPackagesChanged(changedPackages, changedUids.toArray(), userId,
-                    restrictionFlags);
+            mBroadcastHelper.sendDistractingPackagesChanged(mPm.snapshotComputer(),
+                    changedPackages, changedUids.toArray(), userId, restrictionFlags);
             mPm.scheduleWritePackageRestrictions(userId);
         }
         return unactionedPackages.toArray(new String[0]);
@@ -202,34 +198,9 @@
         if (!changedPackages.isEmpty()) {
             final String[] packageArray = changedPackages.toArray(
                     new String[changedPackages.size()]);
-            sendDistractingPackagesChanged(packageArray, changedUids.toArray(), userId,
-                    RESTRICTION_NONE);
+            mBroadcastHelper.sendDistractingPackagesChanged(mPm.snapshotComputer(),
+                    packageArray, changedUids.toArray(), userId, RESTRICTION_NONE);
             mPm.scheduleWritePackageRestrictions(userId);
         }
     }
-
-    /**
-     * Send broadcast intents for packages distracting changes.
-     *
-     * @param pkgList The names of packages which have suspension changes.
-     * @param uidList The uids of packages which have suspension changes.
-     * @param userId The user where packages reside.
-     */
-    void sendDistractingPackagesChanged(@NonNull String[] pkgList, int[] uidList, int userId,
-            int distractionFlags) {
-        final Bundle extras = new Bundle();
-        extras.putStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST, pkgList);
-        extras.putIntArray(Intent.EXTRA_CHANGED_UID_LIST, uidList);
-        extras.putInt(Intent.EXTRA_DISTRACTION_RESTRICTIONS, distractionFlags);
-
-        final Handler handler = mInjector.getHandler();
-        handler.post(() -> mBroadcastHelper.sendPackageBroadcast(
-                Intent.ACTION_DISTRACTING_PACKAGES_CHANGED, null /* pkg */,
-                extras, Intent.FLAG_RECEIVER_REGISTERED_ONLY, null /* targetPkg */,
-                null /* finishedReceiver */, new int[]{userId}, null /* instantUserIds */,
-                null /* broadcastAllowList */,
-                (callingUid, intentExtras) -> BroadcastHelper.filterExtrasChangedPackageList(
-                        mPm.snapshotComputer(), callingUid, intentExtras),
-                null /* bOptions */));
-    }
 }
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index e1e5e6d..8f71a9b 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -38,7 +38,6 @@
 import static android.content.pm.PackageManager.UNINSTALL_REASON_UNKNOWN;
 import static android.content.pm.SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V4;
 import static android.content.pm.parsing.ApkLiteParseUtils.isApkFile;
-import static android.os.PowerExemptionManager.REASON_PACKAGE_REPLACED;
 import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER;
 import static android.os.incremental.IncrementalManager.isIncrementalPath;
 import static android.os.storage.StorageManager.FLAG_STORAGE_CE;
@@ -50,7 +49,6 @@
 import static com.android.server.pm.InstructionSets.getDexCodeInstructionSet;
 import static com.android.server.pm.InstructionSets.getPreferredInstructionSet;
 import static com.android.server.pm.PackageManagerService.APP_METADATA_FILE_NAME;
-import static com.android.server.pm.PackageManagerService.DEBUG_BACKUP;
 import static com.android.server.pm.PackageManagerService.DEBUG_COMPRESSION;
 import static com.android.server.pm.PackageManagerService.DEBUG_INSTALL;
 import static com.android.server.pm.PackageManagerService.DEBUG_PACKAGE_SCANNING;
@@ -130,7 +128,6 @@
 import android.net.Uri;
 import android.os.Binder;
 import android.os.Build;
-import android.os.Bundle;
 import android.os.Environment;
 import android.os.FileUtils;
 import android.os.Message;
@@ -143,9 +140,6 @@
 import android.os.UserManager;
 import android.os.incremental.IncrementalManager;
 import android.os.incremental.IncrementalStorage;
-import android.os.storage.StorageManager;
-import android.os.storage.VolumeInfo;
-import android.stats.storage.StorageEnums;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.text.TextUtils;
@@ -163,7 +157,6 @@
 import com.android.internal.security.VerityUtils;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.CollectionUtils;
-import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.EventLogTags;
 import com.android.server.LocalManagerRegistry;
 import com.android.server.SystemConfig;
@@ -220,6 +213,7 @@
     private final AppDataHelper mAppDataHelper;
     private final BroadcastHelper mBroadcastHelper;
     private final RemovePackageHelper mRemovePackageHelper;
+    private final DeletePackageHelper mDeletePackageHelper;
     private final IncrementalManager mIncrementalManager;
     private final ApexManager mApexManager;
     private final DexManager mDexManager;
@@ -233,12 +227,17 @@
     private final UpdateOwnershipHelper mUpdateOwnershipHelper;
 
     // TODO(b/198166813): remove PMS dependency
-    InstallPackageHelper(PackageManagerService pm, AppDataHelper appDataHelper) {
+    InstallPackageHelper(PackageManagerService pm,
+                         AppDataHelper appDataHelper,
+                         RemovePackageHelper removePackageHelper,
+                         DeletePackageHelper deletePackageHelper,
+                         BroadcastHelper broadcastHelper) {
         mPm = pm;
         mInjector = pm.mInjector;
         mAppDataHelper = appDataHelper;
-        mBroadcastHelper = new BroadcastHelper(pm.mInjector);
-        mRemovePackageHelper = new RemovePackageHelper(pm);
+        mBroadcastHelper = broadcastHelper;
+        mRemovePackageHelper = removePackageHelper;
+        mDeletePackageHelper = deletePackageHelper;
         mIncrementalManager = pm.mInjector.getIncrementalManager();
         mApexManager = pm.mInjector.getApexManager();
         mDexManager = pm.mInjector.getDexManager();
@@ -251,10 +250,6 @@
         mUpdateOwnershipHelper = pm.mInjector.getUpdateOwnershipHelper();
     }
 
-    InstallPackageHelper(PackageManagerService pm) {
-        this(pm, new AppDataHelper(pm));
-    }
-
     /**
      * Commits the package scan and modifies system state.
      * <p><em>WARNING:</em> The method may throw an exception in the middle
@@ -263,7 +258,7 @@
      * possible and the system is not left in an inconsistent state.
      */
     @GuardedBy("mPm.mLock")
-    public AndroidPackage commitReconciledScanResultLocked(
+    private AndroidPackage commitReconciledScanResultLocked(
             @NonNull ReconciledPackage reconciledPkg, int[] allUsers) {
         final InstallRequest request = reconciledPkg.mInstallRequest;
         // TODO(b/135203078): Move this even further away
@@ -731,8 +726,9 @@
                 }
                 // TODO(b/278553670) Store archive state for the user.
                 boolean isArchived = (pkgSetting.getPkg() == null);
-                mPm.sendPackageAddedForUser(mPm.snapshotComputer(), packageName, pkgSetting, userId,
-                        isArchived, DataLoaderType.NONE);
+                mBroadcastHelper.sendPackageAddedForUser(mPm.snapshotComputer(), packageName,
+                        pkgSetting, userId, isArchived, DataLoaderType.NONE,
+                        mPm.mAppPredictionServicePackage);
                 synchronized (mPm.mLock) {
                     mPm.updateSequenceNumberLP(pkgSetting, new int[]{ userId });
                 }
@@ -1745,7 +1741,7 @@
                 }
 
                 // Update what is removed
-                PackageRemovedInfo removedInfo = new PackageRemovedInfo(mPm);
+                PackageRemovedInfo removedInfo = new PackageRemovedInfo();
                 removedInfo.mUid = ps.getAppId();
                 removedInfo.mRemovedPackage = ps.getPackageName();
                 removedInfo.mInstallerPackageName =
@@ -2074,8 +2070,6 @@
             final InstallRequest installRequest = reconciledPkg.mInstallRequest;
             final ParsedPackage parsedPackage = installRequest.getParsedPackage();
             final String packageName = parsedPackage.getPackageName();
-            final RemovePackageHelper removePackageHelper = new RemovePackageHelper(mPm);
-            final DeletePackageHelper deletePackageHelper = new DeletePackageHelper(mPm);
 
             installRequest.onCommitStarted();
             if (installRequest.isInstallReplace()) {
@@ -2097,7 +2091,7 @@
                                 allUsers, mPm.mSettings.getPackagesLocked());
                 if (installRequest.isInstallSystem()) {
                     // Remove existing system package
-                    removePackageHelper.removePackage(oldPackage, true);
+                    mRemovePackageHelper.removePackage(oldPackage, true);
                     if (!disableSystemPackageLPw(oldPackage)) {
                         // We didn't need to disable the .apk as a current system package,
                         // which means we are replacing another update that is already
@@ -2113,7 +2107,7 @@
                 } else {
                     try {
                         // Settings will be written during the call to updateSettingsLI().
-                        deletePackageHelper.executeDeletePackage(
+                        mDeletePackageHelper.executeDeletePackage(
                                 reconciledPkg.mDeletePackageAction, packageName,
                                 true, allUsers, false);
                     } catch (SystemDeleteException e) {
@@ -2200,12 +2194,19 @@
         final String installerPackageName = installRequest.getInstallerPackageName();
 
         if (DEBUG_INSTALL) Slog.d(TAG, "New package installed in " + pkg.getPath());
+        final int userId = installRequest.getUserId();
+        if (userId != UserHandle.USER_ALL && userId != UserHandle.USER_CURRENT
+                && !mPm.mUserManager.exists(userId)) {
+            installRequest.setError(PackageManagerException.ofInternalError(
+                    "User " + userId + " doesn't exist or has been removed",
+                    PackageManagerException.INTERNAL_ERROR_MISSING_USER));
+            return;
+        }
         synchronized (mPm.mLock) {
             // For system-bundled packages, we assume that installing an upgraded version
             // of the package implies that the user actually wants to run that new code,
             // so we enable the package.
             final PackageSetting ps = mPm.mSettings.getPackageLPr(pkgName);
-            final int userId = installRequest.getUserId();
             if (ps != null) {
                 if (ps.isSystem()) {
                     if (DEBUG_INSTALL) {
@@ -2796,24 +2797,21 @@
         final Computer snapshot = mPm.snapshotComputer();
         // Send broadcasts
         for (int i = 0; i < numBroadcasts; i++) {
-            mPm.sendPackageChangedBroadcast(snapshot, packages[i], true /* dontKillApp */,
-                    components[i], uids[i], null /* reason */);
+            mBroadcastHelper.sendPackageChangedBroadcast(snapshot, packages[i],
+                    true /* dontKillApp */, components[i], uids[i], null /* reason */);
         }
     }
 
     void handlePackagePostInstall(InstallRequest request, boolean launchedForRestore) {
         final boolean killApp =
                 (request.getInstallFlags() & PackageManager.INSTALL_DONT_KILL_APP) == 0;
-        final boolean virtualPreload =
-                ((request.getInstallFlags() & PackageManager.INSTALL_VIRTUAL_PRELOAD) != 0);
-        final String installerPackage = request.getInstallerPackageName();
-        final int dataLoaderType = request.getDataLoaderType();
         final boolean succeeded = request.getReturnCode() == PackageManager.INSTALL_SUCCEEDED;
         final boolean update = request.isUpdate();
         final boolean archived = request.isArchived();
         final String packageName = request.getName();
+        final Computer snapshot = mPm.snapshotComputer();
         final PackageStateInternal pkgSetting =
-                succeeded ? mPm.snapshotComputer().getPackageStateInternal(packageName) : null;
+                succeeded ? snapshot.getPackageStateInternal(packageName) : null;
         final boolean removedBeforeUpdate = (pkgSetting == null)
                 || (pkgSetting.isSystem() && !pkgSetting.getPath().getPath().equals(
                 request.getPkg().getPath()));
@@ -2834,208 +2832,22 @@
             // Clear the uid cache after we installed a new package.
             mPm.mPerUidReadTimeoutsCache = null;
 
-            // Send the removed broadcasts
-            if (request.getRemovedInfo() != null) {
-                if (request.getRemovedInfo().mIsExternal) {
-                    if (DEBUG_INSTALL) {
-                        Slog.i(TAG, "upgrading pkg " + request.getRemovedInfo().mRemovedPackage
-                                + " is ASEC-hosted -> UNAVAILABLE");
-                    }
-                    final String[] pkgNames = new String[]{
-                            request.getRemovedInfo().mRemovedPackage};
-                    final int[] uids = new int[]{request.getRemovedInfo().mUid};
-                    mPm.notifyResourcesChanged(false /* mediaStatus */,
-                            true /* replacing */, pkgNames, uids);
-                    mBroadcastHelper.sendResourcesChangedBroadcast(mPm::snapshotComputer,
-                            false /* mediaStatus */, true /* replacing */, pkgNames, uids);
-                }
-                request.getRemovedInfo().sendPackageRemovedBroadcasts(
-                        killApp, false /*removedBySystem*/, false /*isArchived*/);
-            }
-
-            final String installerPackageName =
-                    request.getInstallerPackageName() != null
-                            ? request.getInstallerPackageName()
-                            : request.getRemovedInfo() != null
-                                    ? request.getRemovedInfo().mInstallerPackageName
-                                    : null;
-
             mPm.notifyInstantAppPackageInstalled(request.getPkg().getPackageName(),
                     request.getNewUsers());
 
             request.populateBroadcastUsers();
             final int[] firstUserIds = request.getFirstTimeBroadcastUserIds();
-            final int[] firstInstantUserIds = request.getFirstTimeBroadcastInstantUserIds();
-            final int[] updateUserIds = request.getUpdateBroadcastUserIds();
-            final int[] instantUserIds = request.getUpdateBroadcastInstantUserIds();
 
-            Bundle extras = new Bundle();
-            extras.putInt(Intent.EXTRA_UID, request.getAppId());
-            if (update) {
-                extras.putBoolean(Intent.EXTRA_REPLACING, true);
-            }
-            if (archived) {
-                extras.putBoolean(Intent.EXTRA_ARCHIVAL, true);
-            }
-            extras.putInt(PackageInstaller.EXTRA_DATA_LOADER_TYPE, dataLoaderType);
-
-            // If a package is a static shared library, then only the installer of the package
-            // should get the broadcast.
-            if (installerPackageName != null
-                    && request.getPkg().getStaticSharedLibraryName() != null) {
-                mPm.sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED, packageName,
-                        extras, 0 /*flags*/,
-                        installerPackageName, null /*finishedReceiver*/,
-                        request.getNewUsers(), null /* instantUserIds*/,
-                        null /* broadcastAllowList */, null);
-            }
-
-            // Send installed broadcasts if the package is not a static shared lib.
             if (request.getPkg().getStaticSharedLibraryName() == null) {
                 mPm.mProcessLoggingHandler.invalidateBaseApkHash(request.getPkg().getBaseApkPath());
-
-                // Send PACKAGE_ADDED broadcast for users that see the package for the first time
-                // sendPackageAddedForNewUsers also deals with system apps
-                int appId = UserHandle.getAppId(request.getAppId());
-                boolean isSystem = request.isInstallSystem();
-                mPm.sendPackageAddedForNewUsers(mPm.snapshotComputer(), packageName,
-                        isSystem || virtualPreload, virtualPreload /*startReceiver*/, appId,
-                        firstUserIds, firstInstantUserIds, archived, dataLoaderType);
-
-                // Send PACKAGE_ADDED broadcast for users that don't see
-                // the package for the first time
-
-                // Send to all running apps.
-                final SparseArray<int[]> newBroadcastAllowList;
-                synchronized (mPm.mLock) {
-                    final Computer snapshot = mPm.snapshotComputer();
-                    newBroadcastAllowList = mPm.mAppsFilter.getVisibilityAllowList(snapshot,
-                            snapshot.getPackageStateInternal(packageName, Process.SYSTEM_UID),
-                            updateUserIds, mPm.mSettings.getPackagesLocked());
-                }
-                mPm.sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED, packageName,
-                        extras, 0 /*flags*/,
-                        null /*targetPackage*/, null /*finishedReceiver*/,
-                        updateUserIds, instantUserIds, newBroadcastAllowList, null);
-                // Send to the installer, even if it's not running.
-                if (installerPackageName != null) {
-                    mPm.sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED, packageName,
-                            extras, 0 /*flags*/,
-                            installerPackageName, null /*finishedReceiver*/,
-                            updateUserIds, instantUserIds, null /* broadcastAllowList */, null);
-                }
-                // Send to PermissionController for all update users, even if it may not be running
-                // for some users
-                if (BroadcastHelper.isPrivacySafetyLabelChangeNotificationsEnabled(mContext)) {
-                    mPm.sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED, packageName,
-                            extras, 0 /*flags*/,
-                            mPm.mRequiredPermissionControllerPackage, null /*finishedReceiver*/,
-                            updateUserIds, instantUserIds, null /* broadcastAllowList */, null);
-                }
-                // Notify required verifier(s) that are not the installer of record for the package.
-                for (String verifierPackageName : mPm.mRequiredVerifierPackages) {
-                    if (verifierPackageName != null && !verifierPackageName.equals(
-                            installerPackageName)) {
-                        mPm.sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED, packageName,
-                                extras, 0 /*flags*/,
-                                verifierPackageName, null /*finishedReceiver*/,
-                                updateUserIds, instantUserIds, null /* broadcastAllowList */,
-                                null);
-                    }
-                }
-                // If package installer is defined, notify package installer about new
-                // app installed
-                mPm.sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED, packageName,
-                        extras, Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND /*flags*/,
-                        mPm.mRequiredInstallerPackage, null /*finishedReceiver*/,
-                        firstUserIds, instantUserIds, null /* broadcastAllowList */, null);
-
-                // Send replaced for users that don't see the package for the first time
-                if (update) {
-                    mPm.sendPackageBroadcast(Intent.ACTION_PACKAGE_REPLACED,
-                            packageName, extras, 0 /*flags*/,
-                            null /*targetPackage*/, null /*finishedReceiver*/,
-                            updateUserIds, instantUserIds,
-                            request.getRemovedInfo().mBroadcastAllowList, null);
-                    if (installerPackageName != null) {
-                        mPm.sendPackageBroadcast(Intent.ACTION_PACKAGE_REPLACED, packageName,
-                                extras, 0 /*flags*/,
-                                installerPackageName, null /*finishedReceiver*/,
-                                updateUserIds, instantUserIds, null /*broadcastAllowList*/,
-                                null);
-                    }
-                    for (String verifierPackageName : mPm.mRequiredVerifierPackages) {
-                        if (verifierPackageName != null && !verifierPackageName.equals(
-                                installerPackageName)) {
-                            mPm.sendPackageBroadcast(Intent.ACTION_PACKAGE_REPLACED,
-                                    packageName, extras, 0 /*flags*/, verifierPackageName,
-                                    null /*finishedReceiver*/, updateUserIds, instantUserIds,
-                                    null /*broadcastAllowList*/, null);
-                        }
-                    }
-                    mPm.sendPackageBroadcast(Intent.ACTION_MY_PACKAGE_REPLACED,
-                            null /*package*/, null /*extras*/, 0 /*flags*/,
-                            packageName /*targetPackage*/,
-                            null /*finishedReceiver*/, updateUserIds, instantUserIds,
-                            null /*broadcastAllowList*/,
-                            mBroadcastHelper.getTemporaryAppAllowlistBroadcastOptions(
-                                    REASON_PACKAGE_REPLACED).toBundle());
-                } else if (launchedForRestore && !request.isInstallSystem()) {
-                    // First-install and we did a restore, so we're responsible for the
-                    // first-launch broadcast.
-                    if (DEBUG_BACKUP) {
-                        Slog.i(TAG, "Post-restore of " + packageName
-                                + " sending FIRST_LAUNCH in " + Arrays.toString(firstUserIds));
-                    }
-                    mBroadcastHelper.sendFirstLaunchBroadcast(packageName, installerPackage,
-                            firstUserIds, firstInstantUserIds);
-                }
-
-                // Send broadcast package appeared if external for all users
-                if (request.getPkg().isExternalStorage()) {
-                    if (!update) {
-                        final StorageManager storageManager =
-                                mInjector.getSystemService(StorageManager.class);
-                        VolumeInfo volume =
-                                storageManager.findVolumeByUuid(
-                                        StorageManager.convert(
-                                                request.getPkg().getVolumeUuid()).toString());
-                        int packageExternalStorageType =
-                                PackageManagerServiceUtils.getPackageExternalStorageType(volume,
-                                        request.getPkg().isExternalStorage());
-                        // If the package was installed externally, log it.
-                        if (packageExternalStorageType != StorageEnums.UNKNOWN) {
-                            FrameworkStatsLog.write(
-                                    FrameworkStatsLog.APP_INSTALL_ON_EXTERNAL_STORAGE_REPORTED,
-                                    packageExternalStorageType, packageName);
-                        }
-                    }
-                    if (DEBUG_INSTALL) {
-                        Slog.i(TAG, "upgrading pkg " + request.getPkg() + " is external");
-                    }
-                    if (!archived) {
-                        final String[] pkgNames = new String[]{packageName};
-                        final int[] uids = new int[]{request.getPkg().getUid()};
-                        mBroadcastHelper.sendResourcesChangedBroadcast(mPm::snapshotComputer,
-                                true /* mediaStatus */, true /* replacing */, pkgNames, uids);
-                        mPm.notifyResourcesChanged(true /* mediaStatus */, true /* replacing */,
-                                pkgNames, uids);
-                    }
-                }
-            } else if (!ArrayUtils.isEmpty(request.getLibraryConsumers())) { // if static shared lib
-                // No need to kill consumers if it's installation of new version static shared lib.
-                final Computer snapshot = mPm.snapshotComputer();
-                final boolean dontKillApp = !update
-                        && request.getPkg().getStaticSharedLibraryName() != null;
-                for (int i = 0; i < request.getLibraryConsumers().size(); i++) {
-                    AndroidPackage pkg = request.getLibraryConsumers().get(i);
-                    // send broadcast that all consumers of the static shared library have changed
-                    mPm.sendPackageChangedBroadcast(snapshot, pkg.getPackageName(), dontKillApp,
-                            new ArrayList<>(Collections.singletonList(pkg.getPackageName())),
-                            pkg.getUid(), null);
-                }
             }
 
+            mBroadcastHelper.sendPostInstallBroadcasts(mPm.snapshotComputer(), request, packageName,
+                    mPm.mRequiredPermissionControllerPackage, mPm.mRequiredVerifierPackages,
+                    mPm.mRequiredInstallerPackage,
+                    /* packageSender= */ mPm, launchedForRestore, killApp, update, archived);
+
+
             // Work that needs to happen on first install within each user
             if (firstUserIds.length > 0) {
                 for (int userId : firstUserIds) {
@@ -3074,7 +2886,6 @@
             }
 
             if (!archived) {
-                final Computer snapshot = mPm.snapshotComputer();
                 // Notify DexManager that the package was installed for new users.
                 // The updated users should already be indexed and the package code paths
                 // should not change.
@@ -3090,7 +2901,7 @@
                 }
             } else {
                 // Now send PACKAGE_REMOVED + EXTRA_REPLACING broadcast.
-                final PackageRemovedInfo info = new PackageRemovedInfo(mPm);
+                final PackageRemovedInfo info = new PackageRemovedInfo();
                 info.mRemovedPackage = packageName;
                 info.mInstallerPackageName = request.getInstallerPackageName();
                 info.mRemovedUsers = firstUserIds;
@@ -3099,8 +2910,8 @@
                 info.mRemovedPackageVersionCode = request.getPkg().getLongVersionCode();
                 info.mRemovedForAllUsers = true;
 
-                info.sendPackageRemovedBroadcasts(false /*killApp*/,
-                        false /*removedBySystem*/, true /*isArchived*/);
+                mBroadcastHelper.sendPackageRemovedBroadcasts(info, mPm,
+                        false /*killApp*/, false /*removedBySystem*/, true /*isArchived*/);
             }
         }
 
@@ -3291,15 +3102,14 @@
         synchronized (mPm.mLock) {
             mPm.mSettings.disableSystemPackageLPw(stubPkg.getPackageName(), true /*replaced*/);
         }
-        final RemovePackageHelper removePackageHelper = new RemovePackageHelper(mPm);
-        removePackageHelper.removePackage(stubPkg, true /*chatty*/);
+        mRemovePackageHelper.removePackage(stubPkg, true /*chatty*/);
         try {
             return initPackageTracedLI(scanFile, parseFlags, scanFlags);
         } catch (PackageManagerException e) {
             Slog.w(TAG, "Failed to install compressed system package:" + stubPkg.getPackageName(),
                     e);
             // Remove the failed install
-            removePackageHelper.removeCodePath(scanFile);
+            mRemovePackageHelper.removeCodePath(scanFile);
             throw e;
         }
     }
@@ -3342,7 +3152,7 @@
             if (!dstCodePath.exists()) {
                 return null;
             }
-            new RemovePackageHelper(mPm).removeCodePath(dstCodePath);
+            mRemovePackageHelper.removeCodePath(dstCodePath);
             return null;
         }
 
@@ -4298,8 +4108,8 @@
                         parsedPackage.getPackageName(), UserHandle.USER_ALL,
                         "scanPackageInternalLI", ApplicationExitInfo.REASON_OTHER,
                         null /* request */)) {
-                    DeletePackageHelper deletePackageHelper = new DeletePackageHelper(mPm);
-                    deletePackageHelper.deletePackageLIF(parsedPackage.getPackageName(), null, true,
+                    mDeletePackageHelper.deletePackageLIF(
+                            parsedPackage.getPackageName(), null, true,
                             mPm.mUserManager.getUserIds(), 0, null, false);
                 }
             } else if (newPkgVersionGreater || newSharedUserSetting) {
diff --git a/services/core/java/com/android/server/pm/InstallingSession.java b/services/core/java/com/android/server/pm/InstallingSession.java
index fe6a8a1..ca8dc29 100644
--- a/services/core/java/com/android/server/pm/InstallingSession.java
+++ b/services/core/java/com/android/server/pm/InstallingSession.java
@@ -94,8 +94,6 @@
     private final UserHandle mUser;
     @NonNull
     final PackageManagerService mPm;
-    final InstallPackageHelper mInstallPackageHelper;
-    final RemovePackageHelper mRemovePackageHelper;
     final boolean mIsInherit;
     final int mSessionId;
     final int mRequireUserAction;
@@ -108,8 +106,6 @@
             PackageLite packageLite, PackageManagerService pm) {
         mPm = pm;
         mUser = user;
-        mInstallPackageHelper = new InstallPackageHelper(mPm);
-        mRemovePackageHelper = new RemovePackageHelper(mPm);
         mOriginInfo = originInfo;
         mMoveInfo = moveInfo;
         mObserver = observer;
@@ -142,8 +138,6 @@
             PackageLite packageLite, PackageManagerService pm) {
         mPm = pm;
         mUser = user;
-        mInstallPackageHelper = new InstallPackageHelper(mPm);
-        mRemovePackageHelper = new RemovePackageHelper(mPm);
         mOriginInfo = OriginInfo.fromStagedFile(stagedDir);
         mMoveInfo = null;
         mInstallReason = fixUpInstallReason(
@@ -242,7 +236,7 @@
         // state can change within this delay and hence we need to re-verify certain conditions.
         boolean isStaged = (mInstallFlags & INSTALL_STAGED) != 0;
         if (isStaged) {
-            Pair<Integer, String> ret = mInstallPackageHelper.verifyReplacingVersionCode(
+            Pair<Integer, String> ret = mPm.verifyReplacingVersionCode(
                     pkgLite, mRequiredInstalledVersionCode, mInstallFlags);
             mRet = ret.first;
             if (mRet != INSTALL_SUCCEEDED) {
@@ -540,39 +534,39 @@
                 }
             }
         } else {
-            mInstallPackageHelper.installPackagesTraced(installRequests);
+            mPm.installPackagesTraced(installRequests);
 
             for (InstallRequest request : installRequests) {
                 doPostInstall(request);
             }
         }
         for (InstallRequest request : installRequests) {
-            mInstallPackageHelper.restoreAndPostInstall(request);
+            mPm.restoreAndPostInstall(request);
         }
     }
 
     private void doPostInstall(InstallRequest request) {
         if (mMoveInfo != null) {
             if (request.getReturnCode() == PackageManager.INSTALL_SUCCEEDED) {
-                mRemovePackageHelper.cleanUpForMoveInstall(mMoveInfo.mFromUuid,
+                mPm.cleanUpForMoveInstall(mMoveInfo.mFromUuid,
                         mMoveInfo.mPackageName, mMoveInfo.mFromCodePath);
             } else {
-                mRemovePackageHelper.cleanUpForMoveInstall(mMoveInfo.mToUuid,
+                mPm.cleanUpForMoveInstall(mMoveInfo.mToUuid,
                         mMoveInfo.mPackageName, mMoveInfo.mFromCodePath);
             }
         } else {
             if (request.getReturnCode() != PackageManager.INSTALL_SUCCEEDED) {
-                mRemovePackageHelper.removeCodePath(request.getCodeFile());
+                mPm.removeCodePath(request.getCodeFile());
             }
         }
     }
 
     private void cleanUpForFailedInstall(InstallRequest request) {
         if (request.isInstallMove()) {
-            mRemovePackageHelper.cleanUpForMoveInstall(request.getMoveToUuid(),
+            mPm.cleanUpForMoveInstall(request.getMoveToUuid(),
                     request.getMovePackageName(), request.getMoveFromCodePath());
         } else {
-            mRemovePackageHelper.removeCodePath(request.getCodeFile());
+            mPm.removeCodePath(request.getCodeFile());
         }
     }
 
diff --git a/services/core/java/com/android/server/pm/PackageHandler.java b/services/core/java/com/android/server/pm/PackageHandler.java
index b4ca477..4ecbd15 100644
--- a/services/core/java/com/android/server/pm/PackageHandler.java
+++ b/services/core/java/com/android/server/pm/PackageHandler.java
@@ -60,14 +60,10 @@
  */
 final class PackageHandler extends Handler {
     private final PackageManagerService mPm;
-    private final InstallPackageHelper mInstallPackageHelper;
-    private final RemovePackageHelper mRemovePackageHelper;
 
     PackageHandler(Looper looper, PackageManagerService pm) {
         super(looper);
         mPm = pm;
-        mInstallPackageHelper = new InstallPackageHelper(mPm);
-        mRemovePackageHelper = new RemovePackageHelper(mPm);
     }
 
     @Override
@@ -82,7 +78,7 @@
     void doHandleMessage(Message msg) {
         switch (msg.what) {
             case SEND_PENDING_BROADCAST: {
-                mInstallPackageHelper.sendPendingBroadcasts();
+                mPm.sendPendingBroadcasts();
                 break;
             }
             case POST_INSTALL: {
@@ -96,7 +92,7 @@
                 request.onInstallCompleted();
                 request.runPostInstallRunnable();
                 if (!request.isInstallExistingForUser()) {
-                    mInstallPackageHelper.handlePackagePostInstall(request, didRestore);
+                    mPm.handlePackagePostInstall(request, didRestore);
                 } else if (DEBUG_INSTALL) {
                     // No post-install when we run restore from installExistingPackageForUser
                     Slog.i(TAG, "Nothing to do for post-install token " + msg.arg1);
@@ -107,7 +103,7 @@
             case DEFERRED_NO_KILL_POST_DELETE: {
                 InstallArgs args = (InstallArgs) msg.obj;
                 if (args != null) {
-                    mRemovePackageHelper.cleanUpResources(args.mCodeFile, args.mInstructionSets);
+                    mPm.cleanUpResources(args.mCodeFile, args.mInstructionSets);
                 }
             } break;
             case DEFERRED_NO_KILL_INSTALL_OBSERVER:
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index 95b565d..1bb20b47 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -18,7 +18,9 @@
 
 import static android.app.admin.DevicePolicyResources.Strings.Core.PACKAGE_DELETED_BY_DO;
 import static android.os.Process.INVALID_UID;
+
 import static com.android.server.pm.PackageManagerService.SHELL_PACKAGE_NAME;
+
 import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
 import static org.xmlpull.v1.XmlPullParser.START_TAG;
 
@@ -407,11 +409,10 @@
     }
 
     private void removeStagingDirs(ArraySet<File> stagingDirsToRemove) {
-        final RemovePackageHelper removePackageHelper = new RemovePackageHelper(mPm);
         // Clean up orphaned staging directories
         for (File stage : stagingDirsToRemove) {
             Slog.w(TAG, "Deleting orphan stage " + stage);
-            removePackageHelper.removeCodePath(stage);
+            mPm.removeCodePath(stage);
         }
     }
 
@@ -1320,9 +1321,8 @@
     @Override
     public void installExistingPackage(String packageName, int installFlags, int installReason,
             IntentSender statusReceiver, int userId, List<String> allowListedPermissions) {
-        final InstallPackageHelper installPackageHelper = new InstallPackageHelper(mPm);
 
-        var result = installPackageHelper.installExistingPackageAsUser(packageName, userId,
+        var result = mPm.installExistingPackageAsUser(packageName, userId,
                 installFlags, installReason, allowListedPermissions, statusReceiver);
 
         int returnCode = result.first;
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 54a2e3ad..d0e5f96 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -2449,14 +2449,13 @@
     }
 
     private void onSystemDataLoaderUnrecoverable() {
-        final DeletePackageHelper deletePackageHelper = new DeletePackageHelper(mPm);
         final String packageName = getPackageName();
         if (TextUtils.isEmpty(packageName)) {
             // The package has not been installed.
             return;
         }
         mHandler.post(() -> {
-            if (deletePackageHelper.deletePackageX(packageName,
+            if (mPm.deletePackageX(packageName,
                     PackageManager.VERSION_CODE_HIGHEST, UserHandle.USER_SYSTEM,
                     PackageManager.DELETE_ALL_USERS, true /*removedBySystem*/)
                     != PackageManager.DELETE_SUCCEEDED) {
diff --git a/services/core/java/com/android/server/pm/PackageManagerException.java b/services/core/java/com/android/server/pm/PackageManagerException.java
index dea6659..d69737a 100644
--- a/services/core/java/com/android/server/pm/PackageManagerException.java
+++ b/services/core/java/com/android/server/pm/PackageManagerException.java
@@ -63,6 +63,7 @@
     public static final int INTERNAL_ERROR_STATIC_SHARED_LIB_OVERLAY_TARGETS = -35;
     public static final int INTERNAL_ERROR_APEX_NOT_DIRECTORY = -36;
     public static final int INTERNAL_ERROR_APEX_MORE_THAN_ONE_FILE = -37;
+    public static final int INTERNAL_ERROR_MISSING_USER = -38;
 
     @IntDef(prefix = { "INTERNAL_ERROR_" }, value = {
             INTERNAL_ERROR_NATIVE_LIBRARY_COPY,
@@ -101,7 +102,8 @@
             INTERNAL_ERROR_STATIC_SHARED_LIB_PROTECTED_BROADCAST,
             INTERNAL_ERROR_STATIC_SHARED_LIB_OVERLAY_TARGETS,
             INTERNAL_ERROR_APEX_NOT_DIRECTORY,
-            INTERNAL_ERROR_APEX_MORE_THAN_ONE_FILE
+            INTERNAL_ERROR_APEX_MORE_THAN_ONE_FILE,
+            INTERNAL_ERROR_MISSING_USER
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface InternalErrorCode {}
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 33cb85c..ddc8369 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -70,7 +70,6 @@
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
-import android.content.IIntentReceiver;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.IntentSender;
@@ -98,6 +97,7 @@
 import android.content.pm.InstantAppRequest;
 import android.content.pm.ModuleInfo;
 import android.content.pm.PackageInfo;
+import android.content.pm.PackageInfoLite;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.ComponentEnabledSetting;
@@ -115,6 +115,7 @@
 import android.content.pm.UserInfo;
 import android.content.pm.UserPackage;
 import android.content.pm.VerifierDeviceIdentity;
+import android.content.pm.VerifierInfo;
 import android.content.pm.VersionedPackage;
 import android.content.pm.overlay.OverlayPaths;
 import android.content.pm.parsing.PackageLite;
@@ -985,7 +986,7 @@
     private final DeletePackageHelper mDeletePackageHelper;
     private final InitAppsHelper mInitAppsHelper;
     private final AppDataHelper mAppDataHelper;
-    private final InstallPackageHelper mInstallPackageHelper;
+    @NonNull private final InstallPackageHelper mInstallPackageHelper;
     private final PreferredActivityHelper mPreferredActivityHelper;
     private final ResolveIntentHelper mResolveIntentHelper;
     private final DexOptHelper mDexOptHelper;
@@ -1715,7 +1716,8 @@
                 (i, pm) -> new CrossProfileIntentFilterHelper(i.getSettings(),
                         i.getUserManagerService(), i.getLock(), i.getUserManagerInternal(),
                         context),
-                (i, pm) -> new UpdateOwnershipHelper());
+                (i, pm) -> new UpdateOwnershipHelper(),
+                (i, pm) -> new PackageMonitorCallbackHelper());
 
         if (Build.VERSION.SDK_INT <= 0) {
             Slog.w(TAG, "**** ro.build.version.sdk not set!");
@@ -2067,17 +2069,19 @@
         mDomainVerificationManager.setConnection(mDomainVerificationConnection);
 
         mBroadcastHelper = new BroadcastHelper(mInjector);
-        mPackageMonitorCallbackHelper = new PackageMonitorCallbackHelper(mInjector);
+        mPackageMonitorCallbackHelper = injector.getPackageMonitorCallbackHelper();
         mAppDataHelper = new AppDataHelper(this);
-        mInstallPackageHelper = new InstallPackageHelper(this, mAppDataHelper);
-        mRemovePackageHelper = new RemovePackageHelper(this, mAppDataHelper);
-        mDeletePackageHelper = new DeletePackageHelper(this, mRemovePackageHelper);
+        mRemovePackageHelper = new RemovePackageHelper(this, mAppDataHelper, mBroadcastHelper);
+        mDeletePackageHelper = new DeletePackageHelper(this, mRemovePackageHelper,
+                mBroadcastHelper);
+        mInstallPackageHelper = new InstallPackageHelper(this, mAppDataHelper, mRemovePackageHelper,
+                mDeletePackageHelper, mBroadcastHelper);
 
         mInstantAppRegistry = new InstantAppRegistry(mContext, mPermissionManager,
                 mInjector.getUserManagerInternal(), mDeletePackageHelper);
 
         mSharedLibraries.setDeletePackageHelper(mDeletePackageHelper);
-        mPreferredActivityHelper = new PreferredActivityHelper(this);
+        mPreferredActivityHelper = new PreferredActivityHelper(this, mBroadcastHelper);
         mResolveIntentHelper = new ResolveIntentHelper(mContext, mPreferredActivityHelper,
                 injector.getCompatibility(), mUserManager, mDomainVerificationManager,
                 mUserNeedsBadging, () -> mResolveInfo, () -> mInstantAppInstallerActivity,
@@ -2085,7 +2089,7 @@
         mDexOptHelper = new DexOptHelper(this);
         mSuspendPackageHelper = new SuspendPackageHelper(this, mInjector, mUserManager,
                 mBroadcastHelper, mProtectedPackages);
-        mDistractingPackageHelper = new DistractingPackageHelper(this, mInjector, mBroadcastHelper,
+        mDistractingPackageHelper = new DistractingPackageHelper(this, mBroadcastHelper,
                 mSuspendPackageHelper);
         mStorageEventHelper = new StorageEventHelper(this, mDeletePackageHelper,
                 mRemovePackageHelper);
@@ -3078,38 +3082,6 @@
     }
 
     @Override
-    public void sendPackageBroadcast(final String action, final String pkg, final Bundle extras,
-            final int flags, final String targetPkg, final IIntentReceiver finishedReceiver,
-            final int[] userIds, int[] instantUserIds,
-            @Nullable SparseArray<int[]> broadcastAllowList,
-            @Nullable Bundle bOptions) {
-        mHandler.post(() -> mBroadcastHelper.sendPackageBroadcast(action, pkg, extras, flags,
-                targetPkg, finishedReceiver, userIds, instantUserIds, broadcastAllowList,
-                null /* filterExtrasForReceiver */, bOptions));
-        if (targetPkg == null) {
-            // For some broadcast action, e.g. ACTION_PACKAGE_ADDED, this method will be called
-            // many times to different targets, e.g. installer app, permission controller, other
-            // registered apps. We should filter it to avoid calling back many times for the same
-            // action. When the targetPkg is set, it sends the broadcast to specific app, e.g.
-            // installer app or null for registered apps. The callback only need to send back to the
-            // registered apps so we check the null condition here.
-            notifyPackageMonitor(action, pkg, extras, userIds, instantUserIds, broadcastAllowList);
-        }
-    }
-
-    void notifyPackageMonitor(String action, String pkg, Bundle extras, int[] userIds,
-            int[] instantUserIds, SparseArray<int[]> broadcastAllowList) {
-        mPackageMonitorCallbackHelper.notifyPackageMonitor(action, pkg, extras, userIds,
-                instantUserIds, broadcastAllowList);
-    }
-
-    void notifyResourcesChanged(boolean mediaStatus, boolean replacing,
-            @NonNull String[] pkgNames, @NonNull int[] uids) {
-        mPackageMonitorCallbackHelper.notifyResourcesChanged(mediaStatus, replacing, pkgNames,
-                uids);
-    }
-
-    @Override
     public void notifyPackageAdded(String packageName, int uid) {
         mPackageObserverHelper.notifyAdded(packageName, uid);
     }
@@ -3125,64 +3097,6 @@
         UserPackage.removeFromCache(UserHandle.getUserId(uid), packageName);
     }
 
-    void sendPackageAddedForUser(@NonNull Computer snapshot, String packageName,
-            @NonNull PackageStateInternal packageState, int userId, boolean isArchived,
-            int dataLoaderType) {
-        final PackageUserStateInternal userState = packageState.getUserStateOrDefault(userId);
-        final boolean isSystem = packageState.isSystem();
-        final boolean isInstantApp = userState.isInstantApp();
-        final int[] userIds = isInstantApp ? EMPTY_INT_ARRAY : new int[] { userId };
-        final int[] instantUserIds = isInstantApp ? new int[] { userId } : EMPTY_INT_ARRAY;
-        sendPackageAddedForNewUsers(snapshot, packageName, isSystem /*sendBootCompleted*/,
-                false /*startReceiver*/, packageState.getAppId(), userIds, instantUserIds,
-                isArchived, dataLoaderType);
-
-        // Send a session commit broadcast
-        final PackageInstaller.SessionInfo info = new PackageInstaller.SessionInfo();
-        info.installReason = userState.getInstallReason();
-        info.appPackageName = packageName;
-        sendSessionCommitBroadcast(info, userId);
-    }
-
-    @Override
-    public void sendPackageAddedForNewUsers(@NonNull Computer snapshot, String packageName,
-            boolean sendBootCompleted, boolean includeStopped, @AppIdInt int appId, int[] userIds,
-            int[] instantUserIds, boolean isArchived, int dataLoaderType) {
-        if (ArrayUtils.isEmpty(userIds) && ArrayUtils.isEmpty(instantUserIds)) {
-            return;
-        }
-        SparseArray<int[]> broadcastAllowList = mAppsFilter.getVisibilityAllowList(snapshot,
-                snapshot.getPackageStateInternal(packageName, Process.SYSTEM_UID),
-                userIds, snapshot.getPackageStates());
-        mHandler.post(
-                () -> mBroadcastHelper.sendPackageAddedForNewUsers(packageName, appId, userIds,
-                        instantUserIds, isArchived, dataLoaderType, broadcastAllowList));
-        mPackageMonitorCallbackHelper.notifyPackageAddedForNewUsers(packageName, appId, userIds,
-                instantUserIds, isArchived, dataLoaderType, broadcastAllowList);
-        if (sendBootCompleted && !ArrayUtils.isEmpty(userIds)) {
-            mHandler.post(() -> {
-                        for (int userId : userIds) {
-                            mBroadcastHelper.sendBootCompletedBroadcastToSystemApp(
-                                    packageName, includeStopped, userId);
-                        }
-                    }
-            );
-        }
-    }
-
-    private void sendApplicationHiddenForUser(String packageName, PackageStateInternal packageState,
-            int userId) {
-        final PackageRemovedInfo info = new PackageRemovedInfo(this);
-        info.mRemovedPackage = packageName;
-        info.mInstallerPackageName = packageState.getInstallSource().mInstallerPackageName;
-        info.mRemovedUsers = new int[] {userId};
-        info.mBroadcastUsers = new int[] {userId};
-        info.mUid = UserHandle.getUid(userId, packageState.getAppId());
-        info.mRemovedPackageVersionCode = packageState.getVersionCode();
-        info.sendPackageRemovedBroadcasts(true /*killApp*/, false /*removedBySystem*/,
-                false /*isArchived*/);
-    }
-
     boolean isUserRestricted(int userId, String restrictionKey) {
         Bundle restrictions = mUserManager.getUserRestrictions(userId);
         if (restrictions.getBoolean(restrictionKey, false)) {
@@ -3505,11 +3419,6 @@
         }
     }
 
-    void postPreferredActivityChangedBroadcast(int userId) {
-        mHandler.post(() -> mBroadcastHelper.sendPreferredActivityChangedBroadcast(userId));
-    }
-
-
     /** This method takes a specific user id as well as UserHandle.USER_ALL. */
     @GuardedBy("mLock")
     void clearPackagePreferredActivitiesLPw(String packageName,
@@ -3609,17 +3518,8 @@
     }
 
     public void sendSessionCommitBroadcast(PackageInstaller.SessionInfo sessionInfo, int userId) {
-        UserManagerService ums = UserManagerService.getInstance();
-        if (ums == null || sessionInfo.isStaged()) {
-            return;
-        }
-        final UserInfo parent = ums.getProfileParent(userId);
-        final int launcherUid = (parent != null) ? parent.id : userId;
-        // TODO: Should this snapshot be moved further up?
-        final ComponentName launcherComponent = snapshotComputer()
-                .getDefaultHomeActivity(launcherUid);
-        mBroadcastHelper.sendSessionCommitBroadcast(sessionInfo, userId, launcherUid,
-                launcherComponent, mAppPredictionServicePackage);
+        mBroadcastHelper.sendSessionCommitBroadcast(snapshotComputer(), sessionInfo, userId,
+                mAppPredictionServicePackage);
     }
 
     private @Nullable String getSetupWizardPackageNameImpl(@NonNull Computer computer) {
@@ -3988,7 +3888,7 @@
             if (isSystemStub
                     && (newState == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
                     || newState == PackageManager.COMPONENT_ENABLED_STATE_ENABLED)) {
-                if (!mInstallPackageHelper.enableCompressedPackage(deletedPkg, pkgSetting)) {
+                if (!enableCompressedPackage(deletedPkg, pkgSetting)) {
                     Slog.w(TAG, "Failed setApplicationEnabledSetting: failed to enable "
                             + "commpressed package " + setting.getPackageName());
                     updateAllowed[i] = false;
@@ -4070,8 +3970,8 @@
                 final ArrayList<String> components = sendNowBroadcasts.valueAt(i);
                 final int packageUid = UserHandle.getUid(
                         userId, pkgSettings.get(packageName).getAppId());
-                sendPackageChangedBroadcast(newSnapshot, packageName, false /* dontKillApp */,
-                        components, packageUid, null /* reason */);
+                mBroadcastHelper.sendPackageChangedBroadcast(newSnapshot, packageName,
+                        false /* dontKillApp */, components, packageUid, null /* reason */);
             }
         } finally {
             Binder.restoreCallingIdentity(callingId);
@@ -4147,27 +4047,6 @@
         }
     }
 
-    void sendPackageChangedBroadcast(@NonNull Computer snapshot, String packageName,
-            boolean dontKillApp, ArrayList<String> componentNames, int packageUid, String reason) {
-        PackageStateInternal setting = snapshot.getPackageStateInternal(packageName,
-                Process.SYSTEM_UID);
-        if (setting == null) {
-            return;
-        }
-        final int userId = UserHandle.getUserId(packageUid);
-        final boolean isInstantApp =
-                snapshot.isInstantAppInternal(packageName, userId, Process.SYSTEM_UID);
-        final int[] userIds = isInstantApp ? EMPTY_INT_ARRAY : new int[] { userId };
-        final int[] instantUserIds = isInstantApp ? new int[] { userId } : EMPTY_INT_ARRAY;
-        final SparseArray<int[]> broadcastAllowList =
-                isInstantApp ? null : snapshot.getVisibilityAllowLists(packageName, userIds);
-        mHandler.post(() -> mBroadcastHelper.sendPackageChangedBroadcast(
-                packageName, dontKillApp, componentNames, packageUid, reason, userIds,
-                instantUserIds, broadcastAllowList));
-        mPackageMonitorCallbackHelper.notifyPackageChanged(packageName, dontKillApp, componentNames,
-                packageUid, reason, userIds, instantUserIds, broadcastAllowList);
-    }
-
     /**
      * Used by SystemServer
      */
@@ -4312,7 +4191,7 @@
                 if (pkg == null) {
                     return;
                 }
-                sendPackageChangedBroadcast(snapshot, pkg.getPackageName(),
+                mBroadcastHelper.sendPackageChangedBroadcast(snapshot, pkg.getPackageName(),
                         true /* dontKillApp */,
                         new ArrayList<>(Collections.singletonList(pkg.getPackageName())),
                         pkg.getUid(),
@@ -5294,7 +5173,7 @@
                 throw new SecurityException("Calling package " + packageName
                         + " does not belong to calling uid " + callingUid);
             }
-            return mSuspendPackageHelper
+            return SuspendPackageHelper
                     .getSuspendedPackageAppExtras(snapshot, packageName, userId, callingUid);
         }
 
@@ -5857,10 +5736,14 @@
                 if (hidden) {
                     killApplication(packageName, newPackageState.getAppId(), userId, "hiding pkg",
                             ApplicationExitInfo.REASON_OTHER);
-                    sendApplicationHiddenForUser(packageName, newPackageState, userId);
+                    mBroadcastHelper.sendApplicationHiddenForUser(
+                            packageName, newPackageState, userId,
+                            /* packageSender= */ PackageManagerService.this);
                 } else {
-                    sendPackageAddedForUser(newSnapshot, packageName, newPackageState, userId,
-                            false /* isArchived */, DataLoaderType.NONE);
+                    mBroadcastHelper.sendPackageAddedForUser(
+                            newSnapshot, packageName, newPackageState, userId,
+                            false /* isArchived */, DataLoaderType.NONE,
+                            mAppPredictionServicePackage);
                 }
 
                 scheduleWritePackageRestrictions(userId);
@@ -7929,4 +7812,75 @@
             }
         }
     }
+
+    void removeCodePath(@Nullable File codePath) {
+        mRemovePackageHelper.removeCodePath(codePath);
+    }
+
+    void cleanUpResources(@Nullable File codeFile, @Nullable String[] instructionSets) {
+        mRemovePackageHelper.cleanUpResources(codeFile, instructionSets);
+    }
+
+    void cleanUpForMoveInstall(String volumeUuid, String packageName, String fromCodePath) {
+        mRemovePackageHelper.cleanUpForMoveInstall(volumeUuid, packageName, fromCodePath);
+    }
+
+    void sendPendingBroadcasts() {
+        mInstallPackageHelper.sendPendingBroadcasts();
+    }
+
+    void handlePackagePostInstall(@NonNull InstallRequest request, boolean launchedForRestore) {
+        mInstallPackageHelper.handlePackagePostInstall(request, launchedForRestore);
+    }
+
+    Pair<Integer, IntentSender> installExistingPackageAsUser(
+            @Nullable String packageName,
+            @UserIdInt int userId, @PackageManager.InstallFlags int installFlags,
+            @PackageManager.InstallReason int installReason,
+            @Nullable List<String> allowlistedRestrictedPermissions,
+            @Nullable IntentSender intentSender) {
+        return mInstallPackageHelper.installExistingPackageAsUser(packageName, userId, installFlags,
+                installReason, allowlistedRestrictedPermissions, intentSender);
+    }
+    AndroidPackage initPackageTracedLI(File scanFile, final int parseFlags, int scanFlags)
+            throws PackageManagerException {
+        return mInstallPackageHelper.initPackageTracedLI(scanFile, parseFlags, scanFlags);
+    }
+
+    void restoreDisabledSystemPackageLIF(@NonNull DeletePackageAction action,
+                                         @NonNull int[] allUserHandles,
+                                         boolean writeSettings) throws SystemDeleteException {
+        mInstallPackageHelper.restoreDisabledSystemPackageLIF(
+                action, allUserHandles, writeSettings);
+    }
+    boolean enableCompressedPackage(@NonNull AndroidPackage stubPkg,
+                                    @NonNull PackageSetting stubPs) {
+        return mInstallPackageHelper.enableCompressedPackage(stubPkg, stubPs);
+    }
+
+    void installPackagesTraced(List<InstallRequest> requests) {
+        mInstallPackageHelper.installPackagesTraced(requests);
+    }
+
+    void restoreAndPostInstall(InstallRequest request) {
+        mInstallPackageHelper.restoreAndPostInstall(request);
+    }
+
+    Pair<Integer, String> verifyReplacingVersionCode(@NonNull PackageInfoLite pkgLite,
+                                    long requiredInstalledVersionCode,
+                                    int installFlags) {
+        return mInstallPackageHelper.verifyReplacingVersionCode(
+                pkgLite, requiredInstalledVersionCode, installFlags);
+    }
+
+    int getUidForVerifier(VerifierInfo verifierInfo) {
+        return mInstallPackageHelper.getUidForVerifier(verifierInfo);
+    }
+
+    int deletePackageX(String packageName, long versionCode, int userId, int deleteFlags,
+                        boolean removedBySystem) {
+        return mDeletePackageHelper.deletePackageX(packageName,
+                PackageManager.VERSION_CODE_HIGHEST, UserHandle.USER_SYSTEM,
+                PackageManager.DELETE_ALL_USERS, true /*removedBySystem*/);
+    }
 }
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java b/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java
index 0c2e082..5b770aab 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java
@@ -146,6 +146,7 @@
     private final Singleton<SharedLibrariesImpl> mSharedLibrariesProducer;
     private final Singleton<CrossProfileIntentFilterHelper> mCrossProfileIntentFilterHelperProducer;
     private final Singleton<UpdateOwnershipHelper> mUpdateOwnershipHelperProducer;
+    private final Singleton<PackageMonitorCallbackHelper> mPackageMonitorCallbackHelper;
 
     PackageManagerServiceInjector(Context context, PackageManagerTracedLock lock,
             Installer installer, Object installLock, PackageAbiHelper abiHelper,
@@ -186,7 +187,8 @@
             Producer<IBackupManager> iBackupManager,
             Producer<SharedLibrariesImpl> sharedLibrariesProducer,
             Producer<CrossProfileIntentFilterHelper> crossProfileIntentFilterHelperProducer,
-            Producer<UpdateOwnershipHelper> updateOwnershipHelperProducer) {
+            Producer<UpdateOwnershipHelper> updateOwnershipHelperProducer,
+            Producer<PackageMonitorCallbackHelper> packageMonitorCallbackHelper) {
         mContext = context;
         mLock = lock;
         mInstaller = installer;
@@ -242,6 +244,7 @@
         mCrossProfileIntentFilterHelperProducer = new Singleton<>(
                 crossProfileIntentFilterHelperProducer);
         mUpdateOwnershipHelperProducer = new Singleton<>(updateOwnershipHelperProducer);
+        mPackageMonitorCallbackHelper = new Singleton<>(packageMonitorCallbackHelper);
     }
 
     /**
@@ -431,6 +434,10 @@
         return mUpdateOwnershipHelperProducer.get(this, mPackageManager);
     }
 
+    public PackageMonitorCallbackHelper getPackageMonitorCallbackHelper() {
+        return mPackageMonitorCallbackHelper.get(this, mPackageManager);
+    }
+
 
     /** Provides an abstraction to static access to system state. */
     public interface SystemWrapper {
diff --git a/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java b/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java
index bb3bf53..b8c2b86 100644
--- a/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java
+++ b/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java
@@ -52,12 +52,6 @@
     private final Object mLock = new Object();
     final IActivityManager mActivityManager = ActivityManager.getService();
 
-    final Handler mHandler;
-
-    PackageMonitorCallbackHelper(PackageManagerServiceInjector injector) {
-        mHandler = injector.getHandler();
-    }
-
     @NonNull
     @GuardedBy("mLock")
     private final RemoteCallbackList<IRemoteCallback> mCallbacks = new RemoteCallbackList<>();
@@ -100,7 +94,8 @@
 
     public void notifyPackageAddedForNewUsers(String packageName,
             @AppIdInt int appId, @NonNull int[] userIds, @NonNull int[] instantUserIds,
-            boolean isArchived, int dataLoaderType, SparseArray<int[]> broadcastAllowList) {
+            boolean isArchived, int dataLoaderType, SparseArray<int[]> broadcastAllowList,
+            @NonNull Handler handler) {
         Bundle extras = new Bundle(2);
         // Set to UID of the first user, EXTRA_UID is automatically updated in sendPackageBroadcast
         final int uid = UserHandle.getUid(
@@ -111,11 +106,11 @@
         }
         extras.putInt(PackageInstaller.EXTRA_DATA_LOADER_TYPE, dataLoaderType);
         notifyPackageMonitor(Intent.ACTION_PACKAGE_ADDED, packageName, extras ,
-                userIds /* userIds */, instantUserIds, broadcastAllowList);
+                userIds /* userIds */, instantUserIds, broadcastAllowList, handler);
     }
 
     public void notifyResourcesChanged(boolean mediaStatus, boolean replacing,
-            @NonNull String[] pkgNames, @NonNull int[] uids) {
+            @NonNull String[] pkgNames, @NonNull int[] uids, @NonNull Handler handler) {
         Bundle extras = new Bundle();
         extras.putStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST, pkgNames);
         extras.putIntArray(Intent.EXTRA_CHANGED_UID_LIST, uids);
@@ -125,12 +120,12 @@
         String action = mediaStatus ? Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE
                 : Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE;
         notifyPackageMonitor(action, null /* pkg */, extras, null /* userIds */,
-                null /* instantUserIds */, null /* broadcastAllowList */);
+                null /* instantUserIds */, null /* broadcastAllowList */, handler);
     }
 
     public void notifyPackageChanged(String packageName, boolean dontKillApp,
             ArrayList<String> componentNames, int packageUid, String reason, int[] userIds,
-            int[] instantUserIds, SparseArray<int[]> broadcastAllowList) {
+            int[] instantUserIds, SparseArray<int[]> broadcastAllowList, Handler handler) {
         Bundle extras = new Bundle(4);
         extras.putString(Intent.EXTRA_CHANGED_COMPONENT_NAME, componentNames.get(0));
         String[] nameList = new String[componentNames.size()];
@@ -142,11 +137,12 @@
             extras.putString(Intent.EXTRA_REASON, reason);
         }
         notifyPackageMonitor(Intent.ACTION_PACKAGE_CHANGED, packageName, extras, userIds,
-                instantUserIds, broadcastAllowList);
+                instantUserIds, broadcastAllowList, handler);
     }
 
     public void notifyPackageMonitor(String action, String pkg, Bundle extras,
-            int[] userIds, int[] instantUserIds, SparseArray<int[]> broadcastAllowList) {
+            int[] userIds, int[] instantUserIds, SparseArray<int[]> broadcastAllowList,
+            Handler handler) {
         if (!isAllowedCallbackAction(action)) {
             return;
         }
@@ -160,9 +156,10 @@
             }
 
             if (ArrayUtils.isEmpty(instantUserIds)) {
-                doNotifyCallbacks(action, pkg, extras, resolvedUserIds, broadcastAllowList);
+                doNotifyCallbacks(
+                        action, pkg, extras, resolvedUserIds, broadcastAllowList, handler);
             } else {
-                doNotifyCallbacks(action, pkg, extras, instantUserIds, broadcastAllowList);
+                doNotifyCallbacks(action, pkg, extras, instantUserIds, broadcastAllowList, handler);
             }
         } catch (RemoteException e) {
             // do nothing
@@ -181,7 +178,7 @@
     }
 
     private void doNotifyCallbacks(String action, String pkg, Bundle extras, int[] userIds,
-            SparseArray<int[]> broadcastAllowList) {
+            SparseArray<int[]> broadcastAllowList, Handler handler) {
         RemoteCallbackList<IRemoteCallback> callbacks;
         synchronized (mLock) {
             callbacks = mCallbacks;
@@ -202,7 +199,7 @@
             final int[] allowUids =
                     broadcastAllowList != null ? broadcastAllowList.get(userId) : new int[]{};
 
-            mHandler.post(() -> callbacks.broadcast((callback, user) -> {
+            handler.post(() -> callbacks.broadcast((callback, user) -> {
                 RegisterUser registerUser = (RegisterUser) user;
                 if ((registerUser.getUserId() != UserHandle.USER_ALL) && (registerUser.getUserId()
                         != userId)) {
diff --git a/services/core/java/com/android/server/pm/PackageRemovedInfo.java b/services/core/java/com/android/server/pm/PackageRemovedInfo.java
index 9f02542..7ee1772 100644
--- a/services/core/java/com/android/server/pm/PackageRemovedInfo.java
+++ b/services/core/java/com/android/server/pm/PackageRemovedInfo.java
@@ -16,24 +16,12 @@
 
 package com.android.server.pm;
 
-import static android.os.PowerExemptionManager.REASON_PACKAGE_REPLACED;
-import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED;
-import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
-
-import android.annotation.NonNull;
-import android.app.ActivityManagerInternal;
-import android.app.BroadcastOptions;
-import android.content.Intent;
-import android.os.Bundle;
-import android.os.PowerExemptionManager;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
 
 import com.android.internal.util.ArrayUtils;
-import com.android.server.LocalServices;
 
 final class PackageRemovedInfo {
-    final PackageSender mPackageSender;
     String mRemovedPackage;
     String mInstallerPackageName;
     int mUid = -1;
@@ -58,116 +46,6 @@
     InstallArgs mArgs = null;
     private static final int[] EMPTY_INT_ARRAY = new int[0];
 
-    PackageRemovedInfo(PackageSender packageSender) {
-        mPackageSender = packageSender;
-    }
-
-    void sendPackageRemovedBroadcasts(boolean killApp, boolean removedBySystem,
-            boolean isArchived) {
-        sendPackageRemovedBroadcastInternal(killApp, removedBySystem, isArchived);
-    }
-
-    void sendSystemPackageUpdatedBroadcasts() {
-        if (mIsRemovedPackageSystemUpdate) {
-            sendSystemPackageUpdatedBroadcastsInternal();
-        }
-    }
-
-    private void sendSystemPackageUpdatedBroadcastsInternal() {
-        Bundle extras = new Bundle(2);
-        extras.putInt(Intent.EXTRA_UID, mRemovedAppId >= 0 ? mRemovedAppId : mUid);
-        extras.putBoolean(Intent.EXTRA_REPLACING, true);
-        mPackageSender.sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED, mRemovedPackage, extras,
-                0, null /*targetPackage*/, null, null, null, mBroadcastAllowList, null);
-        if (mInstallerPackageName != null) {
-            mPackageSender.sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED,
-                    mRemovedPackage, extras, 0 /*flags*/,
-                    mInstallerPackageName, null, null, null, null /* broadcastAllowList */,
-                    null);
-            mPackageSender.sendPackageBroadcast(Intent.ACTION_PACKAGE_REPLACED,
-                    mRemovedPackage, extras, 0 /*flags*/,
-                    mInstallerPackageName, null, null, null, null /* broadcastAllowList */,
-                    null);
-        }
-        mPackageSender.sendPackageBroadcast(Intent.ACTION_PACKAGE_REPLACED, mRemovedPackage,
-                extras, 0, null /*targetPackage*/, null, null, null, mBroadcastAllowList, null);
-        mPackageSender.sendPackageBroadcast(Intent.ACTION_MY_PACKAGE_REPLACED, null, null, 0,
-                mRemovedPackage, null, null, null, null /* broadcastAllowList */,
-                getTemporaryAppAllowlistBroadcastOptions(REASON_PACKAGE_REPLACED).toBundle());
-    }
-
-    private static @NonNull BroadcastOptions getTemporaryAppAllowlistBroadcastOptions(
-            @PowerExemptionManager.ReasonCode int reasonCode) {
-        long duration = 10_000;
-        final ActivityManagerInternal amInternal =
-                LocalServices.getService(ActivityManagerInternal.class);
-        if (amInternal != null) {
-            duration = amInternal.getBootTimeTempAllowListDuration();
-        }
-        final BroadcastOptions bOptions = BroadcastOptions.makeBasic();
-        bOptions.setTemporaryAppAllowlist(duration,
-                TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
-                reasonCode, "");
-        return bOptions;
-    }
-
-    private void sendPackageRemovedBroadcastInternal(boolean killApp, boolean removedBySystem,
-            boolean isArchived) {
-        Bundle extras = new Bundle();
-        final int removedUid = mRemovedAppId >= 0  ? mRemovedAppId : mUid;
-        extras.putInt(Intent.EXTRA_UID, removedUid);
-        extras.putBoolean(Intent.EXTRA_DATA_REMOVED, mDataRemoved);
-        extras.putBoolean(Intent.EXTRA_SYSTEM_UPDATE_UNINSTALL, mIsRemovedPackageSystemUpdate);
-        extras.putBoolean(Intent.EXTRA_DONT_KILL_APP, !killApp);
-        extras.putBoolean(Intent.EXTRA_USER_INITIATED, !removedBySystem);
-        final boolean isReplace = mIsUpdate || mIsRemovedPackageSystemUpdate;
-        if (isReplace || isArchived) {
-            extras.putBoolean(Intent.EXTRA_REPLACING, true);
-        }
-        if (isArchived) {
-            extras.putBoolean(Intent.EXTRA_ARCHIVAL, true);
-        }
-        extras.putBoolean(Intent.EXTRA_REMOVED_FOR_ALL_USERS, mRemovedForAllUsers);
-
-        // Send PACKAGE_REMOVED broadcast to the respective installer.
-        if (mRemovedPackage != null && mInstallerPackageName != null) {
-            mPackageSender.sendPackageBroadcast(Intent.ACTION_PACKAGE_REMOVED,
-                    mRemovedPackage, extras, 0 /*flags*/,
-                    mInstallerPackageName, null, mBroadcastUsers, mInstantUserIds, null, null);
-        }
-        if (mIsStaticSharedLib) {
-            // When uninstalling static shared libraries, only the package's installer needs to be
-            // sent a PACKAGE_REMOVED broadcast. There are no other intended recipients.
-            return;
-        }
-        if (mRemovedPackage != null) {
-            mPackageSender.sendPackageBroadcast(Intent.ACTION_PACKAGE_REMOVED,
-                    mRemovedPackage, extras, 0, null /*targetPackage*/, null,
-                    mBroadcastUsers, mInstantUserIds, mBroadcastAllowList, null);
-            mPackageSender.sendPackageBroadcast(Intent.ACTION_PACKAGE_REMOVED_INTERNAL,
-                    mRemovedPackage, extras, 0 /*flags*/, PLATFORM_PACKAGE_NAME,
-                    null /*finishedReceiver*/, mBroadcastUsers, mInstantUserIds,
-                    mBroadcastAllowList, null /*bOptions*/);
-            if (mDataRemoved && !mIsRemovedPackageSystemUpdate) {
-                mPackageSender.sendPackageBroadcast(Intent.ACTION_PACKAGE_FULLY_REMOVED,
-                        mRemovedPackage, extras, Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND, null,
-                        null, mBroadcastUsers, mInstantUserIds, mBroadcastAllowList, null);
-                mPackageSender.notifyPackageRemoved(mRemovedPackage, removedUid);
-            }
-        }
-        if (mRemovedAppId >= 0) {
-            // If a system app's updates are uninstalled the UID is not actually removed. Some
-            // services need to know the package name affected.
-            if (isReplace) {
-                extras.putString(Intent.EXTRA_PACKAGE_NAME, mRemovedPackage);
-            }
-
-            mPackageSender.sendPackageBroadcast(Intent.ACTION_UID_REMOVED,
-                    null, extras, Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND,
-                    null, null, mBroadcastUsers, mInstantUserIds, mBroadcastAllowList, null);
-        }
-    }
-
     public void populateBroadcastUsers(PackageSetting deletedPackageSetting) {
         if (mRemovedUsers == null) {
             mBroadcastUsers = null;
diff --git a/services/core/java/com/android/server/pm/PackageSender.java b/services/core/java/com/android/server/pm/PackageSender.java
index 82e1d5f3..db83f59 100644
--- a/services/core/java/com/android/server/pm/PackageSender.java
+++ b/services/core/java/com/android/server/pm/PackageSender.java
@@ -16,24 +16,7 @@
 
 package com.android.server.pm;
 
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.content.IIntentReceiver;
-import android.os.Bundle;
-import android.util.SparseArray;
-
 interface PackageSender {
-    /**
-     * @param userIds User IDs where the action occurred on a full application
-     * @param instantUserIds User IDs where the action occurred on an instant application
-     */
-    void sendPackageBroadcast(String action, String pkg,
-            Bundle extras, int flags, String targetPkg,
-            IIntentReceiver finishedReceiver, int[] userIds, int[] instantUserIds,
-            @Nullable SparseArray<int[]> broadcastAllowList, @Nullable Bundle bOptions);
-    void sendPackageAddedForNewUsers(@NonNull Computer snapshot, String packageName,
-            boolean sendBootCompleted, boolean includeStopped, int appId, int[] userIds,
-            int[] instantUserIds, boolean isArchived, int dataLoaderType);
     void notifyPackageAdded(String packageName, int uid);
     void notifyPackageChanged(String packageName, int uid);
     void notifyPackageRemoved(String packageName, int uid);
diff --git a/services/core/java/com/android/server/pm/PreferredActivityHelper.java b/services/core/java/com/android/server/pm/PreferredActivityHelper.java
index 571aab4..41d2aeb 100644
--- a/services/core/java/com/android/server/pm/PreferredActivityHelper.java
+++ b/services/core/java/com/android/server/pm/PreferredActivityHelper.java
@@ -69,10 +69,12 @@
     private static final String TAG_DEFAULT_APPS = "da";
 
     private final PackageManagerService mPm;
+    private final BroadcastHelper mBroadcastHelper;
 
     // TODO(b/198166813): remove PMS dependency
-    PreferredActivityHelper(PackageManagerService pm) {
+    PreferredActivityHelper(PackageManagerService pm, BroadcastHelper broadcastHelper) {
         mPm = pm;
+        mBroadcastHelper = broadcastHelper;
     }
 
     private ResolveInfo findPreferredActivityNotLocked(@NonNull Computer snapshot, Intent intent,
@@ -120,7 +122,7 @@
         }
         if (changedUsers.size() > 0) {
             updateDefaultHomeNotLocked(mPm.snapshotComputer(), changedUsers);
-            mPm.postPreferredActivityChangedBroadcast(userId);
+            mBroadcastHelper.sendPreferredActivityChangedBroadcast(userId);
             mPm.scheduleWritePackageRestrictions(userId);
         }
     }
@@ -167,7 +169,7 @@
         return mPm.setActiveLauncherPackage(packageName, userId,
                 successful -> {
                     if (successful) {
-                        mPm.postPreferredActivityChangedBroadcast(userId);
+                        mBroadcastHelper.sendPreferredActivityChangedBroadcast(userId);
                     }
                 });
     }
@@ -215,7 +217,7 @@
         }
         // Re-snapshot after mLock
         if (!(isHomeFilter(filter) && updateDefaultHomeNotLocked(mPm.snapshotComputer(), userId))) {
-            mPm.postPreferredActivityChangedBroadcast(userId);
+            mBroadcastHelper.sendPreferredActivityChangedBroadcast(userId);
         }
     }
 
@@ -411,7 +413,7 @@
         if (isHomeFilter(filter)) {
             updateDefaultHomeNotLocked(mPm.snapshotComputer(), userId);
         }
-        mPm.postPreferredActivityChangedBroadcast(userId);
+        mBroadcastHelper.sendPreferredActivityChangedBroadcast(userId);
     }
 
     public void clearPackagePersistentPreferredActivities(String packageName, int userId) {
@@ -426,7 +428,7 @@
         }
         if (changed) {
             updateDefaultHomeNotLocked(mPm.snapshotComputer(), userId);
-            mPm.postPreferredActivityChangedBroadcast(userId);
+            mBroadcastHelper.sendPreferredActivityChangedBroadcast(userId);
             mPm.scheduleWritePackageRestrictions(userId);
         }
     }
@@ -443,7 +445,7 @@
         }
         if (changed) {
             updateDefaultHomeNotLocked(mPm.snapshotComputer(), userId);
-            mPm.postPreferredActivityChangedBroadcast(userId);
+            mBroadcastHelper.sendPreferredActivityChangedBroadcast(userId);
             mPm.scheduleWritePackageRestrictions(userId);
         }
     }
@@ -616,7 +618,7 @@
                 mPm.clearPackagePreferredActivitiesLPw(null, changedUsers, userId);
             }
             if (changedUsers.size() > 0) {
-                mPm.postPreferredActivityChangedBroadcast(userId);
+                mBroadcastHelper.sendPreferredActivityChangedBroadcast(userId);
             }
             synchronized (mPm.mLock) {
                 mPm.mSettings.applyDefaultPreferredAppsLPw(userId);
diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java
index d989c90..b055a3f 100644
--- a/services/core/java/com/android/server/pm/RemovePackageHelper.java
+++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java
@@ -22,6 +22,7 @@
 import static android.os.storage.StorageManager.FLAG_STORAGE_CE;
 import static android.os.storage.StorageManager.FLAG_STORAGE_DE;
 import static android.os.storage.StorageManager.FLAG_STORAGE_EXTERNAL;
+
 import static com.android.server.pm.InstructionSets.getDexCodeInstructionSets;
 import static com.android.server.pm.PackageManagerService.DEBUG_INSTALL;
 import static com.android.server.pm.PackageManagerService.DEBUG_REMOVE;
@@ -69,9 +70,11 @@
     private final PermissionManagerServiceInternal mPermissionManager;
     private final SharedLibrariesImpl mSharedLibraries;
     private final AppDataHelper mAppDataHelper;
+    private final BroadcastHelper mBroadcastHelper;
 
     // TODO(b/198166813): remove PMS dependency
-    RemovePackageHelper(PackageManagerService pm, AppDataHelper appDataHelper) {
+    RemovePackageHelper(PackageManagerService pm, AppDataHelper appDataHelper,
+                        BroadcastHelper broadcastHelper) {
         mPm = pm;
         mIncrementalManager = mPm.mInjector.getIncrementalManager();
         mInstaller = mPm.mInjector.getInstaller();
@@ -79,10 +82,7 @@
         mPermissionManager = mPm.mInjector.getPermissionManagerServiceInternal();
         mSharedLibraries = mPm.mInjector.getSharedLibrariesImpl();
         mAppDataHelper = appDataHelper;
-    }
-
-    RemovePackageHelper(PackageManagerService pm) {
-        this(pm, new AppDataHelper(pm));
+        mBroadcastHelper = broadcastHelper;
     }
 
     public void removeCodePath(File codePath) {
@@ -265,7 +265,8 @@
 
         final List<AndroidPackage> sharedUserPkgs =
                 sus != null ? sus.getPackages() : Collections.emptyList();
-        final PreferredActivityHelper preferredActivityHelper = new PreferredActivityHelper(mPm);
+        final PreferredActivityHelper preferredActivityHelper = new PreferredActivityHelper(mPm,
+                mBroadcastHelper);
         final int[] userIds = (userId == UserHandle.USER_ALL) ? mUserManagerInternal.getUserIds()
                 : new int[] {userId};
         for (int nextUserId : userIds) {
@@ -395,13 +396,13 @@
             }
             if (changedUsers.size() > 0) {
                 final PreferredActivityHelper preferredActivityHelper =
-                        new PreferredActivityHelper(mPm);
+                        new PreferredActivityHelper(mPm, mBroadcastHelper);
                 preferredActivityHelper.updateDefaultHomeNotLocked(mPm.snapshotComputer(),
                         changedUsers);
-                mPm.postPreferredActivityChangedBroadcast(UserHandle.USER_ALL);
+                mBroadcastHelper.sendPreferredActivityChangedBroadcast(UserHandle.USER_ALL);
             }
         } else if (!deletedPs.isSystem() && outInfo != null && !outInfo.mIsUpdate
-                && outInfo.mRemovedUsers != null) {
+                && outInfo.mRemovedUsers != null && !outInfo.mIsExternal) {
             // For non-system uninstalls with DELETE_KEEP_DATA, set the installed state to false
             // for affected users. This does not apply to app updates where the old apk is replaced
             // but the old data remains.
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index 0ea45c4..e993d9e 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -372,6 +372,7 @@
         // Extract Icon and update the icon res ID and the bitmap path.
         s.saveIconAndFixUpShortcutLocked(this, newShortcut);
         s.fixUpShortcutResourceNamesAndValues(newShortcut);
+        ensureShortcutCountBeforePush();
         saveShortcut(newShortcut);
     }
 
@@ -426,7 +427,6 @@
             @NonNull List<ShortcutInfo> changedShortcuts) {
         Preconditions.checkArgument(newShortcut.isEnabled(),
                 "pushDynamicShortcuts() cannot publish disabled shortcuts");
-        ensureShortcutCountBeforePush();
 
         newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC);
 
diff --git a/services/core/java/com/android/server/pm/StorageEventHelper.java b/services/core/java/com/android/server/pm/StorageEventHelper.java
index db5b9b1..c725cdc 100644
--- a/services/core/java/com/android/server/pm/StorageEventHelper.java
+++ b/services/core/java/com/android/server/pm/StorageEventHelper.java
@@ -147,7 +147,6 @@
 
         final Settings.VersionInfo ver;
         final List<? extends PackageStateInternal> packages;
-        final InstallPackageHelper installPackageHelper = new InstallPackageHelper(mPm);
         synchronized (mPm.mLock) {
             ver = mPm.mSettings.findOrCreateVersion(volumeUuid);
             packages = mPm.mSettings.getVolumePackagesLPr(volumeUuid);
@@ -160,7 +159,7 @@
             synchronized (mPm.mInstallLock) {
                 final AndroidPackage pkg;
                 try {
-                    pkg = installPackageHelper.initPackageTracedLI(
+                    pkg = mPm.initPackageTracedLI(
                             ps.getPath(), parseFlags, SCAN_INITIAL);
                     loaded.add(pkg);
 
@@ -228,7 +227,8 @@
         }
 
         if (DEBUG_INSTALL) Slog.d(TAG, "Loaded packages " + loaded);
-        sendResourcesChangedBroadcast(true /* mediaStatus */, false /* replacing */, loaded);
+        mBroadcastHelper.sendResourcesChangedBroadcastAndNotify(mPm.snapshotComputer(),
+                true /* mediaStatus */, false /* replacing */, loaded);
         synchronized (mLoadedVolumes) {
             mLoadedVolumes.add(vol.getId());
         }
@@ -256,7 +256,7 @@
 
                     final AndroidPackage pkg = ps.getPkg();
                     final int deleteFlags = PackageManager.DELETE_KEEP_DATA;
-                    final PackageRemovedInfo outInfo = new PackageRemovedInfo(mPm);
+                    final PackageRemovedInfo outInfo = new PackageRemovedInfo();
 
                     try (PackageFreezer freezer = mPm.freezePackageForDelete(ps.getPackageName(),
                              UserHandle.USER_ALL, deleteFlags,
@@ -280,7 +280,8 @@
         }
 
         if (DEBUG_INSTALL) Slog.d(TAG, "Unloaded packages " + unloaded);
-        sendResourcesChangedBroadcast(false /* mediaStatus */, false /* replacing */, unloaded);
+        mBroadcastHelper.sendResourcesChangedBroadcastAndNotify(mPm.snapshotComputer(),
+                false /* mediaStatus */, false /* replacing */, unloaded);
         synchronized (mLoadedVolumes) {
             mLoadedVolumes.remove(vol.getId());
         }
@@ -295,21 +296,6 @@
         }
     }
 
-    private void sendResourcesChangedBroadcast(boolean mediaStatus, boolean replacing,
-            ArrayList<AndroidPackage> packages) {
-        final int size = packages.size();
-        final String[] packageNames = new String[size];
-        final int[] packageUids = new int[size];
-        for (int i = 0; i < size; i++) {
-            final AndroidPackage pkg = packages.get(i);
-            packageNames[i] = pkg.getPackageName();
-            packageUids[i] = pkg.getUid();
-        }
-        mBroadcastHelper.sendResourcesChangedBroadcast(mPm::snapshotComputer, mediaStatus,
-                replacing, packageNames, packageUids);
-        mPm.notifyResourcesChanged(mediaStatus, replacing, packageNames, packageUids);
-    }
-
     /**
      * Examine all apps present on given mounted volume, and destroy apps that
      * aren't expected, either due to uninstallation or reinstallation on
diff --git a/services/core/java/com/android/server/pm/SuspendPackageHelper.java b/services/core/java/com/android/server/pm/SuspendPackageHelper.java
index ddb045d..29d99a73 100644
--- a/services/core/java/com/android/server/pm/SuspendPackageHelper.java
+++ b/services/core/java/com/android/server/pm/SuspendPackageHelper.java
@@ -26,17 +26,13 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
-import android.app.ActivityManager;
 import android.app.AppOpsManager;
-import android.app.BroadcastOptions;
-import android.app.IActivityManager;
 import android.app.admin.DevicePolicyManagerInternal;
 import android.content.Intent;
 import android.content.pm.PackageInfo;
 import android.content.pm.SuspendDialogInfo;
 import android.os.Binder;
 import android.os.Bundle;
-import android.os.Handler;
 import android.os.PersistableBundle;
 import android.os.Process;
 import android.os.UserHandle;
@@ -47,7 +43,6 @@
 import android.util.IntArray;
 import android.util.Slog;
 
-import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.CollectionUtils;
 import com.android.server.LocalServices;
@@ -207,19 +202,22 @@
             }
         });
 
+        final Computer newSnapshot = mPm.snapshotComputer();
         if (!notifyPackagesList.isEmpty()) {
             final String[] changedPackages =
                     notifyPackagesList.toArray(new String[0]);
-            sendPackagesSuspendedForUser(
+            mBroadcastHelper.sendPackagesSuspendedOrUnsuspendedForUser(newSnapshot,
                     suspended ? Intent.ACTION_PACKAGES_SUSPENDED
                             : Intent.ACTION_PACKAGES_UNSUSPENDED,
                     changedPackages, notifyUids.toArray(), quarantined, userId);
-            sendMyPackageSuspendedOrUnsuspended(changedPackages, suspended, userId);
+            mBroadcastHelper.sendMyPackageSuspendedOrUnsuspended(newSnapshot, changedPackages,
+                    suspended, userId);
             mPm.scheduleWritePackageRestrictions(userId);
         }
         // Send the suspension changed broadcast to ensure suspension state is not stale.
         if (!changedPackagesList.isEmpty()) {
-            sendPackagesSuspendedForUser(Intent.ACTION_PACKAGES_SUSPENSION_CHANGED,
+            mBroadcastHelper.sendPackagesSuspendedOrUnsuspendedForUser(newSnapshot,
+                    Intent.ACTION_PACKAGES_SUSPENSION_CHANGED,
                     changedPackagesList.toArray(new String[0]), changedUids.toArray(), quarantined,
                     userId);
         }
@@ -269,8 +267,10 @@
      * @return The app extras of the suspended package.
      */
     @Nullable
-    Bundle getSuspendedPackageAppExtras(@NonNull Computer snapshot, @NonNull String packageName,
-            int userId, int callingUid) {
+    static Bundle getSuspendedPackageAppExtras(@NonNull Computer snapshot,
+                                               @NonNull String packageName,
+                                               int userId,
+                                               int callingUid) {
         final PackageStateInternal ps = snapshot.getPackageStateInternal(packageName, callingUid);
         if (ps == null) {
             return null;
@@ -299,7 +299,7 @@
      *                                   suspensions will be removed.
      * @param userId The user for which the changes are taking place.
      */
-    void removeSuspensionsBySuspendingPackage(@NonNull Computer computer,
+    void removeSuspensionsBySuspendingPackage(@NonNull Computer snapshot,
             @NonNull String[] packagesToChange,
             @NonNull Predicate<String> suspendingPackagePredicate, int userId) {
         final List<String> unsuspendedPackages = new ArrayList<>();
@@ -307,7 +307,7 @@
         final ArrayMap<String, ArraySet<String>> pkgToSuspendingPkgsToCommit = new ArrayMap<>();
         for (String packageName : packagesToChange) {
             final PackageStateInternal packageState =
-                    computer.getPackageStateInternal(packageName);
+                    snapshot.getPackageStateInternal(packageName);
             final PackageUserStateInternal packageUserState = packageState == null
                     ? null : packageState.getUserStateOrDefault(userId);
             if (packageUserState == null || !packageUserState.isSuspended()) {
@@ -350,11 +350,14 @@
         });
 
         mPm.scheduleWritePackageRestrictions(userId);
+        final Computer newSnapshot = mPm.snapshotComputer();
         if (!unsuspendedPackages.isEmpty()) {
             final String[] packageArray = unsuspendedPackages.toArray(
                     new String[unsuspendedPackages.size()]);
-            sendMyPackageSuspendedOrUnsuspended(packageArray, false, userId);
-            sendPackagesSuspendedForUser(Intent.ACTION_PACKAGES_UNSUSPENDED,
+            mBroadcastHelper.sendMyPackageSuspendedOrUnsuspended(newSnapshot, packageArray,
+                    false, userId);
+            mBroadcastHelper.sendPackagesSuspendedOrUnsuspendedForUser(newSnapshot,
+                    Intent.ACTION_PACKAGES_UNSUSPENDED,
                     packageArray, unsuspendedUids.toArray(), false, userId);
         }
     }
@@ -610,38 +613,6 @@
     }
 
     /**
-     * Send broadcast intents for packages suspension changes.
-     *
-     * @param intent The action name of the suspension intent.
-     * @param pkgList The names of packages which have suspension changes.
-     * @param uidList The uids of packages which have suspension changes.
-     * @param userId The user where packages reside.
-     */
-    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
-    void sendPackagesSuspendedForUser(@NonNull String intent, @NonNull String[] pkgList,
-            @NonNull int[] uidList, boolean quarantined, int userId) {
-        final Handler handler = mInjector.getHandler();
-        final Bundle extras = new Bundle(3);
-        extras.putStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST, pkgList);
-        extras.putIntArray(Intent.EXTRA_CHANGED_UID_LIST, uidList);
-        if (quarantined) {
-            extras.putBoolean(Intent.EXTRA_QUARANTINED, true);
-        }
-        final int flags = Intent.FLAG_RECEIVER_REGISTERED_ONLY | Intent.FLAG_RECEIVER_FOREGROUND;
-        final Bundle options = new BroadcastOptions()
-                .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE)
-                .toBundle();
-        handler.post(() -> mBroadcastHelper.sendPackageBroadcast(intent, null /* pkg */,
-                extras, flags, null /* targetPkg */, null /* finishedReceiver */,
-                new int[]{userId}, null /* instantUserIds */, null /* broadcastAllowList */,
-                (callingUid, intentExtras) -> BroadcastHelper.filterExtrasChangedPackageList(
-                        mPm.snapshotComputer(), callingUid, intentExtras),
-                options));
-        mPm.notifyPackageMonitor(intent, null /* pkg */, extras, new int[]{userId},
-                null /* instantUserIds */, null /* broadcastAllowList */);
-    }
-
-    /**
      * Suspends packages on behalf of an admin.
      *
      * @return array of packages that are unsuspendable, either because admin is not allowed to
@@ -756,37 +727,4 @@
         }
         return false;
     }
-
-    private void sendMyPackageSuspendedOrUnsuspended(String[] affectedPackages, boolean suspended,
-            int userId) {
-        final Handler handler = mInjector.getHandler();
-        final String action = suspended
-                ? Intent.ACTION_MY_PACKAGE_SUSPENDED
-                : Intent.ACTION_MY_PACKAGE_UNSUSPENDED;
-        handler.post(() -> {
-            final IActivityManager am = ActivityManager.getService();
-            if (am == null) {
-                Slog.wtf(TAG, "IActivityManager null. Cannot send MY_PACKAGE_ "
-                        + (suspended ? "" : "UN") + "SUSPENDED broadcasts");
-                return;
-            }
-            final int[] targetUserIds = new int[] {userId};
-            final Computer snapshot = mPm.snapshotComputer();
-            for (String packageName : affectedPackages) {
-                final Bundle appExtras = suspended
-                        ? getSuspendedPackageAppExtras(snapshot, packageName, userId, SYSTEM_UID)
-                        : null;
-                final Bundle intentExtras;
-                if (appExtras != null) {
-                    intentExtras = new Bundle(1);
-                    intentExtras.putBundle(Intent.EXTRA_SUSPENDED_PACKAGE_EXTRAS, appExtras);
-                } else {
-                    intentExtras = null;
-                }
-                mBroadcastHelper.doSendBroadcast(action, null, intentExtras,
-                        Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND, packageName, null,
-                        targetUserIds, false, null, null, null);
-            }
-        });
-    }
 }
diff --git a/services/core/java/com/android/server/pm/VerifyingSession.java b/services/core/java/com/android/server/pm/VerifyingSession.java
index 8c73ce8..c6435ae 100644
--- a/services/core/java/com/android/server/pm/VerifyingSession.java
+++ b/services/core/java/com/android/server/pm/VerifyingSession.java
@@ -139,7 +139,6 @@
     private final UserHandle mUser;
     @NonNull
     private final PackageManagerService mPm;
-    private final InstallPackageHelper mInstallPackageHelper;
 
     VerifyingSession(UserHandle user, File stagedDir, IPackageInstallObserver2 observer,
             PackageInstaller.SessionParams sessionParams, InstallSource installSource,
@@ -147,7 +146,6 @@
             boolean userActionRequired, PackageManagerService pm) {
         mPm = pm;
         mUser = user;
-        mInstallPackageHelper = new InstallPackageHelper(mPm);
         mOriginInfo = OriginInfo.fromStagedFile(stagedDir);
         mObserver = observer;
         mInstallFlags = sessionParams.installFlags;
@@ -181,7 +179,7 @@
         PackageInfoLite pkgLite = PackageManagerServiceUtils.getMinimalPackageInfo(mPm.mContext,
                 mPackageLite, mOriginInfo.mResolvedPath, mInstallFlags, mPackageAbiOverride);
 
-        Pair<Integer, String> ret = mInstallPackageHelper.verifyReplacingVersionCode(
+        Pair<Integer, String> ret = mPm.verifyReplacingVersionCode(
                 pkgLite, mRequiredInstalledVersionCode, mInstallFlags);
         setReturnCode(ret.first, ret.second);
         if (mRet != INSTALL_SUCCEEDED) {
@@ -729,7 +727,7 @@
                 continue;
             }
 
-            final int verifierUid = mInstallPackageHelper.getUidForVerifier(verifierInfo);
+            final int verifierUid = mPm.getUidForVerifier(verifierInfo);
             if (verifierUid == -1) {
                 continue;
             }
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 097656c..dfc9b8b 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -198,6 +198,7 @@
 import com.android.internal.accessibility.util.AccessibilityUtils;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.AssistUtils;
+import com.android.internal.display.BrightnessUtils;
 import com.android.internal.inputmethod.SoftInputShowHideReason;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto;
@@ -217,7 +218,6 @@
 import com.android.server.LocalServices;
 import com.android.server.SystemServiceManager;
 import com.android.server.UiThread;
-import com.android.server.display.BrightnessUtils;
 import com.android.server.input.InputManagerInternal;
 import com.android.server.input.KeyboardMetricsCollector;
 import com.android.server.input.KeyboardMetricsCollector.KeyboardLogEvent;
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 40e9c13..88eaafa 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -955,6 +955,17 @@
         }
     }
 
+    public void setTiles(String tiles) {
+        enforceStatusBarOrShell();
+
+        if (mBar != null) {
+            try {
+                mBar.setQsTiles(tiles.split(","));
+            } catch (RemoteException ex) {
+            }
+        }
+    }
+
     public void clickTile(ComponentName component) {
         enforceStatusBarOrShell();
 
diff --git a/services/core/java/com/android/server/statusbar/StatusBarShellCommand.java b/services/core/java/com/android/server/statusbar/StatusBarShellCommand.java
index 11a4976d..d6bf02f 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarShellCommand.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarShellCommand.java
@@ -61,6 +61,8 @@
                     return runAddTile();
                 case "remove-tile":
                     return runRemoveTile();
+                case "set-tiles":
+                    return runSetTiles();
                 case "click-tile":
                     return runClickTile();
                 case "check-support":
@@ -105,6 +107,11 @@
         return 0;
     }
 
+    private int runSetTiles() throws RemoteException {
+        mInterface.setTiles(getNextArgRequired());
+        return 0;
+    }
+
     private int runClickTile() throws RemoteException {
         mInterface.clickTile(ComponentName.unflattenFromString(getNextArgRequired()));
         return 0;
@@ -242,6 +249,9 @@
         pw.println("  remove-tile COMPONENT");
         pw.println("    Remove a TileService of the specified component");
         pw.println("");
+        pw.println("  set-tiles LIST-OF-TILES");
+        pw.println("    Sets the list of tiles as the current Quick Settings tiles");
+        pw.println("");
         pw.println("  click-tile COMPONENT");
         pw.println("    Click on a TileService of the specified component");
         pw.println("");
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index a0c7870..1684724 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -580,7 +580,7 @@
     IBinder mRequestedLaunchingTaskFragmentToken;
 
     // Tracking splash screen status from previous activity
-    boolean mAllowIconSplashScreen = true;
+    boolean mSplashScreenStyleSolidColor = false;
 
     boolean mPauseSchedulePendingForPip = false;
 
@@ -2408,7 +2408,8 @@
     @VisibleForTesting
     boolean addStartingWindow(String pkg, int resolvedTheme, ActivityRecord from, boolean newTask,
             boolean taskSwitch, boolean processRunning, boolean allowTaskSnapshot,
-            boolean activityCreated, boolean allowIcon, boolean activityAllDrawn) {
+            boolean activityCreated, boolean isSimple,
+            boolean activityAllDrawn) {
         // If the display is frozen, we won't do anything until the actual window is
         // displayed so there is no reason to put in the starting window.
         if (!okToDisplay()) {
@@ -2443,8 +2444,8 @@
 
         final int typeParameter = StartingSurfaceController
                 .makeStartingWindowTypeParameter(newTask, taskSwitch, processRunning,
-                        allowTaskSnapshot, activityCreated, allowIcon, useLegacy,
-                        activityAllDrawn, type, packageName, mUserId);
+                        allowTaskSnapshot, activityCreated, isSimple, useLegacy, activityAllDrawn,
+                        type, packageName, mUserId);
 
         if (type == STARTING_WINDOW_TYPE_SNAPSHOT) {
             if (isActivityTypeHome()) {
@@ -6746,7 +6747,7 @@
     void onFirstWindowDrawn(WindowState win) {
         firstWindowDrawn = true;
         // stop tracking
-        mAllowIconSplashScreen = false;
+        mSplashScreenStyleSolidColor = true;
 
         if (mStartingWindow != null) {
             ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Finish starting %s"
@@ -6795,7 +6796,7 @@
     void onStartingWindowDrawn() {
         boolean wasTaskVisible = false;
         if (task != null) {
-            mAllowIconSplashScreen = false;
+            mSplashScreenStyleSolidColor = true;
             wasTaskVisible = !setTaskHasBeenVisible();
         }
 
@@ -7320,32 +7321,19 @@
     }
 
     /**
-     * Checks whether an icon splash screen can be used in the starting window based on the
-     * preference in the {@code options} and this activity's theme, giving higher priority to the
-     * {@code options}'s preference.
-     *
-     * When no preference is specified, a default behaviour is defined:
-     *  - if the activity is started from the home or shell app, an icon can be used
-     *  - if the activity is started from SystemUI, an icon should not be used
-     *  - if there is a launching activity, use its preference
-     *  - if none of the above is met, only use an icon when the activity is started for the first
-     *    time from a System app
-     *
-     * The returned value is sent to WmShell, which will make the final decision on what splash
-     * screen type will be used.
-     *
-     * @return true if an icon can be used in the splash screen
-     *         false when an icon should not be used in the splash screen
+     * @return true if a solid color splash screen must be used
+     *         false when an icon splash screen can be used, but the final decision for whether to
+     *               use an icon or solid color splash screen will be made by WmShell.
      */
-    private boolean canUseIconSplashScreen(ActivityRecord sourceRecord,
+    private boolean shouldUseSolidColorSplashScreen(ActivityRecord sourceRecord,
             boolean startActivity, ActivityOptions options, int resolvedTheme) {
         if (sourceRecord == null && !startActivity) {
-            // Shouldn't use an icon if this activity is not top activity. This could happen when
-            // adding a splash screen window to the warm start activity which is re-create because
-            // top is finishing.
+            // Use simple style if this activity is not top activity. This could happen when adding
+            // a splash screen window to the warm start activity which is re-create because top is
+            // finishing.
             final ActivityRecord above = task.getActivityAbove(this);
             if (above != null) {
-                return false;
+                return true;
             }
         }
 
@@ -7353,33 +7341,32 @@
         final int optionsStyle = options != null ? options.getSplashScreenStyle() :
                 SplashScreen.SPLASH_SCREEN_STYLE_UNDEFINED;
         if (optionsStyle == SplashScreen.SPLASH_SCREEN_STYLE_SOLID_COLOR) {
-            return false;
+            return true;
         } else if (optionsStyle == SplashScreen.SPLASH_SCREEN_STYLE_ICON
                     || isIconStylePreferred(resolvedTheme)) {
-            return true;
+            return false;
         }
 
         // Choose the default behavior when neither the ActivityRecord nor the activity theme have
         // specified a splash screen style.
 
         if (mLaunchSourceType == LAUNCH_SOURCE_TYPE_HOME || launchedFromUid == Process.SHELL_UID) {
-            return true;
-        } else if (mLaunchSourceType == LAUNCH_SOURCE_TYPE_SYSTEMUI) {
             return false;
+        } else if (mLaunchSourceType == LAUNCH_SOURCE_TYPE_SYSTEMUI) {
+            return true;
         } else {
-            // Need to check sourceRecord in case this activity is launched from a service or a
-            // trampoline activity.
+            // Need to check sourceRecord in case this activity is launched from a service.
             if (sourceRecord == null) {
                 sourceRecord = searchCandidateLaunchingActivity();
             }
 
             if (sourceRecord != null) {
-                return sourceRecord.mAllowIconSplashScreen;
+                return sourceRecord.mSplashScreenStyleSolidColor;
             }
 
             // Use an icon if the activity was launched from System for the first start.
-            // Otherwise, can't use an icon splash screen.
-            return mLaunchSourceType == LAUNCH_SOURCE_TYPE_SYSTEM && startActivity;
+            // Otherwise, must use solid color splash screen.
+            return mLaunchSourceType != LAUNCH_SOURCE_TYPE_SYSTEM || !startActivity;
         }
     }
 
@@ -7443,7 +7430,7 @@
         final int resolvedTheme = evaluateStartingWindowTheme(prev, packageName, theme,
                 splashScreenTheme);
 
-        mAllowIconSplashScreen = canUseIconSplashScreen(sourceRecord, startActivity,
+        mSplashScreenStyleSolidColor = shouldUseSolidColorSplashScreen(sourceRecord, startActivity,
                 startOptions, resolvedTheme);
 
         final boolean activityCreated =
@@ -7455,7 +7442,7 @@
 
         final boolean scheduled = addStartingWindow(packageName, resolvedTheme,
                 prev, newTask || newSingleActivity, taskSwitch, processRunning,
-                allowTaskSnapshot(), activityCreated, mAllowIconSplashScreen, allDrawn);
+                allowTaskSnapshot(), activityCreated, mSplashScreenStyleSolidColor, allDrawn);
         if (DEBUG_STARTING_WINDOW_VERBOSE && scheduled) {
             Slog.d(TAG, "Scheduled starting window for " + this);
         }
diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java
index f33ecaa..184de58 100644
--- a/services/core/java/com/android/server/wm/RecentTasks.java
+++ b/services/core/java/com/android/server/wm/RecentTasks.java
@@ -301,9 +301,12 @@
         }
 
         // Always update the reordering time when this is called to ensure that the timeout
-        // is reset
+        // is reset.  Extend this duration when running in tests.
+        final long timeout = ActivityManager.isRunningInUserTestHarness()
+                ? mFreezeTaskListTimeoutMs * 10
+                : mFreezeTaskListTimeoutMs;
         mService.mH.removeCallbacks(mResetFreezeTaskListOnTimeoutRunnable);
-        mService.mH.postDelayed(mResetFreezeTaskListOnTimeoutRunnable, mFreezeTaskListTimeoutMs);
+        mService.mH.postDelayed(mResetFreezeTaskListOnTimeoutRunnable, timeout);
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/StartingSurfaceController.java b/services/core/java/com/android/server/wm/StartingSurfaceController.java
index a0517be..a55c232 100644
--- a/services/core/java/com/android/server/wm/StartingSurfaceController.java
+++ b/services/core/java/com/android/server/wm/StartingSurfaceController.java
@@ -19,12 +19,12 @@
 import static android.window.StartingWindowInfo.TYPE_PARAMETER_ACTIVITY_CREATED;
 import static android.window.StartingWindowInfo.TYPE_PARAMETER_ACTIVITY_DRAWN;
 import static android.window.StartingWindowInfo.TYPE_PARAMETER_ALLOW_HANDLE_SOLID_COLOR_SCREEN;
-import static android.window.StartingWindowInfo.TYPE_PARAMETER_ALLOW_ICON;
 import static android.window.StartingWindowInfo.TYPE_PARAMETER_ALLOW_TASK_SNAPSHOT;
 import static android.window.StartingWindowInfo.TYPE_PARAMETER_LEGACY_SPLASH_SCREEN;
 import static android.window.StartingWindowInfo.TYPE_PARAMETER_NEW_TASK;
 import static android.window.StartingWindowInfo.TYPE_PARAMETER_PROCESS_RUNNING;
 import static android.window.StartingWindowInfo.TYPE_PARAMETER_TASK_SWITCH;
+import static android.window.StartingWindowInfo.TYPE_PARAMETER_USE_SOLID_COLOR_SPLASH_SCREEN;
 
 import static com.android.server.wm.ActivityRecord.STARTING_WINDOW_TYPE_SNAPSHOT;
 import static com.android.server.wm.ActivityRecord.STARTING_WINDOW_TYPE_SPLASH_SCREEN;
@@ -102,7 +102,7 @@
 
     static int makeStartingWindowTypeParameter(boolean newTask, boolean taskSwitch,
             boolean processRunning, boolean allowTaskSnapshot, boolean activityCreated,
-            boolean allowIcon, boolean useLegacy, boolean activityDrawn, int startingWindowType,
+            boolean isSolidColor, boolean useLegacy, boolean activityDrawn, int startingWindowType,
             String packageName, int userId) {
         int parameter = 0;
         if (newTask) {
@@ -120,8 +120,8 @@
         if (activityCreated || startingWindowType == STARTING_WINDOW_TYPE_SNAPSHOT) {
             parameter |= TYPE_PARAMETER_ACTIVITY_CREATED;
         }
-        if (allowIcon) {
-            parameter |= TYPE_PARAMETER_ALLOW_ICON;
+        if (isSolidColor) {
+            parameter |= TYPE_PARAMETER_USE_SOLID_COLOR_SPLASH_SCREEN;
         }
         if (useLegacy) {
             parameter |= TYPE_PARAMETER_LEGACY_SPLASH_SCREEN;
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index a6d285a..3faf8b9 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -5241,16 +5241,17 @@
             applyForcedPropertiesForDefaultDisplay();
             mAnimator.ready();
             mDisplayReady = true;
-            // Reconfigure all displays to make sure that forced properties and
-            // DisplayWindowSettings are applied.
-            mRoot.forAllDisplays(DisplayContent::reconfigureDisplayLocked);
+            mHasWideColorGamutSupport = queryWideColorGamutSupport();
+            mHasHdrSupport = queryHdrSupport();
             mIsTouchDevice = mContext.getPackageManager().hasSystemFeature(
                     PackageManager.FEATURE_TOUCHSCREEN);
             mIsFakeTouchDevice = mContext.getPackageManager().hasSystemFeature(
                     PackageManager.FEATURE_FAKETOUCH);
+            // Reconfigure all displays to make sure that the forced properties and
+            // DisplayWindowSettings are applied. In addition, wide-color/hdr/isTouchDevice also
+            // affect the Configuration.
+            mRoot.forAllDisplays(DisplayContent::reconfigureDisplayLocked);
         }
-
-        mAtmService.updateConfiguration(null /* request to compute config */);
     }
 
     public void systemReady() {
@@ -5258,8 +5259,6 @@
         mPolicy.systemReady();
         mRoot.forAllDisplayPolicies(DisplayPolicy::systemReady);
         mSnapshotController.systemReady();
-        mHasWideColorGamutSupport = queryWideColorGamutSupport();
-        mHasHdrSupport = queryHdrSupport();
         UiThread.getHandler().post(mSettingsObserver::loadSettings);
         IVrManager vrManager = IVrManager.Stub.asInterface(
                 ServiceManager.getService(Context.VR_SERVICE));
@@ -9647,4 +9646,15 @@
             Binder.restoreCallingIdentity(origId);
         }
     }
+
+    /**
+     * Resets the spatial ordering of recents for testing purposes.
+     */
+    void resetFreezeRecentTaskListReordering() {
+        if (!checkCallingPermission(MANAGE_ACTIVITY_TASKS,
+                "resetFreezeRecentTaskListReordering()")) {
+            throw new SecurityException("Requires MANAGE_ACTIVITY_TASKS permission");
+        }
+        mAtmService.getRecentTasks().resetFreezeTaskListReorderingOnTimeout();
+    }
 }
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index 8fad950..fa9a65f 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -142,6 +142,8 @@
                     return runReset(pw);
                 case "disable-blur":
                     return runSetBlurDisabled(pw);
+                case "reset-freeze-recent-tasks":
+                    return runResetFreezeRecentTaskListReordering(pw);
                 case "shell":
                     return runWmShellCommand(pw);
                 default:
@@ -252,6 +254,11 @@
         return 0;
     }
 
+    private int runResetFreezeRecentTaskListReordering(PrintWriter pw) throws RemoteException {
+        mInternal.resetFreezeRecentTaskListReordering();
+        return 0;
+    }
+
     private void printInitialDisplayDensity(PrintWriter pw , int displayId) {
         try {
             final int initialDensity = mInterface.getInitialDisplayDensity(displayId);
@@ -1492,6 +1499,8 @@
         printLetterboxHelp(pw);
         printMultiWindowConfigHelp(pw);
 
+        pw.println("  reset-freeze-recent-tasks");
+        pw.println("    Resets the spatial ordering of the recent tasks list");
         pw.println("  reset [-d DISPLAY_ID]");
         pw.println("    Reset all override settings.");
         if (!IS_USER) {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java
index d0ead14..25e8475 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java
@@ -21,6 +21,7 @@
 import static android.app.admin.PolicyUpdateReceiver.EXTRA_POLICY_UPDATE_RESULT_KEY;
 import static android.app.admin.PolicyUpdateResult.RESULT_FAILURE_CONFLICTING_ADMIN_POLICY;
 import static android.app.admin.PolicyUpdateResult.RESULT_FAILURE_HARDWARE_LIMITATION;
+import static android.app.admin.PolicyUpdateResult.RESULT_FAILURE_STORAGE_LIMIT_REACHED;
 import static android.app.admin.PolicyUpdateResult.RESULT_POLICY_CLEARED;
 import static android.app.admin.PolicyUpdateResult.RESULT_POLICY_SET;
 import static android.content.pm.UserProperties.INHERIT_DEVICE_POLICY_FROM_PARENT;
@@ -51,6 +52,7 @@
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.Environment;
+import android.os.Parcel;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -63,6 +65,7 @@
 import com.android.internal.util.XmlUtils;
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
+import com.android.server.devicepolicy.flags.FlagUtils;
 import com.android.server.utils.Slogf;
 
 import libcore.io.IoUtils;
@@ -117,6 +120,10 @@
      * Map containing the current set of admins in each user with active policies.
      */
     private final SparseArray<Set<EnforcingAdmin>> mEnforcingAdmins;
+    private final SparseArray<HashMap<EnforcingAdmin, Integer>> mAdminPolicySize;
+
+    //TODO(b/295504706) : Speak to security team to decide what to set Policy_Size_Limit
+    private static final int POLICY_SIZE_LIMIT = 99999;
 
     private final DeviceAdminServiceController mDeviceAdminServiceController;
 
@@ -131,6 +138,7 @@
         mLocalPolicies = new SparseArray<>();
         mGlobalPolicies = new HashMap<>();
         mEnforcingAdmins = new SparseArray<>();
+        mAdminPolicySize = new SparseArray<>();
     }
 
     /**
@@ -139,7 +147,6 @@
      *
      * <p>If {@code skipEnforcePolicy} is true, it sets the policies in the internal data structure
      * but doesn't call the enforcing logic.
-     *
      */
     <V> void setLocalPolicy(
             @NonNull PolicyDefinition<V> policyDefinition,
@@ -152,6 +159,12 @@
 
         synchronized (mLock) {
             PolicyState<V> localPolicyState = getLocalPolicyStateLocked(policyDefinition, userId);
+            if (FlagUtils.isDevicePolicySizeTrackingEnabled()) {
+                if (!handleAdminPolicySizeLimit(localPolicyState, enforcingAdmin, value,
+                        policyDefinition, userId)) {
+                    return;
+                }
+            }
 
             if (policyDefinition.isNonCoexistablePolicy()) {
                 setNonCoexistableLocalPolicyLocked(policyDefinition, localPolicyState,
@@ -236,6 +249,7 @@
     }
 
     // TODO: add more documentation on broadcasts/callbacks to use to get current enforced values
+
     /**
      * Set the policy for the provided {@code policyDefinition}
      * (see {@link PolicyDefinition}) and {@code enforcingAdmin} to the provided {@code value}.
@@ -250,6 +264,7 @@
     }
 
     // TODO: add more documentation on broadcasts/callbacks to use to get current enforced values
+
     /**
      * Removes any previously set policy for the provided {@code policyDefinition}
      * (see {@link PolicyDefinition}) and {@code enforcingAdmin}.
@@ -267,6 +282,10 @@
             }
             PolicyState<V> localPolicyState = getLocalPolicyStateLocked(policyDefinition, userId);
 
+            if (FlagUtils.isDevicePolicySizeTrackingEnabled()) {
+                decreasePolicySizeForAdmin(localPolicyState, enforcingAdmin);
+            }
+
             if (policyDefinition.isNonCoexistablePolicy()) {
                 setNonCoexistableLocalPolicyLocked(policyDefinition, localPolicyState,
                         enforcingAdmin, /* value= */ null, userId, /* skipEnforcePolicy= */ false);
@@ -392,6 +411,7 @@
     }
 
     // TODO: add more documentation on broadcasts/callbacks to use to get current enforced values
+
     /**
      * Set the policy for the provided {@code policyDefinition}
      * (see {@link PolicyDefinition}) and {@code enforcingAdmin} to the provided {@code value}.
@@ -407,6 +427,13 @@
         Objects.requireNonNull(value);
 
         synchronized (mLock) {
+            PolicyState<V> globalPolicyState = getGlobalPolicyStateLocked(policyDefinition);
+            if (FlagUtils.isDevicePolicySizeTrackingEnabled()) {
+                if (!handleAdminPolicySizeLimit(globalPolicyState, enforcingAdmin, value,
+                        policyDefinition, UserHandle.USER_ALL)) {
+                    return;
+                }
+            }
             // TODO(b/270999567): Move error handling for DISALLOW_CELLULAR_2G into the code
             //  that honors the restriction once there's an API available
             if (checkFor2gFailure(policyDefinition, enforcingAdmin)) {
@@ -416,8 +443,6 @@
                 return;
             }
 
-            PolicyState<V> globalPolicyState = getGlobalPolicyStateLocked(policyDefinition);
-
             boolean policyChanged = globalPolicyState.addPolicy(enforcingAdmin, value);
             boolean policyAppliedOnAllUsers = applyGlobalPolicyOnUsersWithLocalPoliciesLocked(
                     policyDefinition, enforcingAdmin, value, skipEnforcePolicy);
@@ -434,7 +459,7 @@
                 // TODO(b/285532044): remove hack and handle properly
                 if (!policyAppliedGlobally
                         && policyDefinition.getPolicyKey().getIdentifier().equals(
-                                USER_CONTROL_DISABLED_PACKAGES_POLICY)) {
+                        USER_CONTROL_DISABLED_PACKAGES_POLICY)) {
                     PolicyValue<Set<String>> parsedValue = (PolicyValue<Set<String>>) value;
                     PolicyValue<Set<String>> parsedResolvedValue =
                             (PolicyValue<Set<String>>) globalPolicyState.getCurrentResolvedPolicy();
@@ -459,6 +484,7 @@
     }
 
     // TODO: add more documentation on broadcasts/callbacks to use to get current enforced values
+
     /**
      * Removes any previously set policy for the provided {@code policyDefinition}
      * (see {@link PolicyDefinition}) and {@code enforcingAdmin}.
@@ -472,6 +498,11 @@
 
         synchronized (mLock) {
             PolicyState<V> policyState = getGlobalPolicyStateLocked(policyDefinition);
+
+            if (FlagUtils.isDevicePolicySizeTrackingEnabled()) {
+                decreasePolicySizeForAdmin(policyState, enforcingAdmin);
+            }
+
             boolean policyChanged = policyState.removePolicy(enforcingAdmin);
 
             if (policyChanged) {
@@ -687,7 +718,6 @@
      * <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(
@@ -723,7 +753,6 @@
      * <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> getLocalPolicyKeysSetByAllAdmins(
@@ -964,7 +993,7 @@
             EnforcingAdmin callingAdmin,
             PolicyDefinition<V> policyDefinition,
             int userId) {
-        for (EnforcingAdmin admin: policyState.getPoliciesSetByAdmins().keySet()) {
+        for (EnforcingAdmin admin : policyState.getPoliciesSetByAdmins().keySet()) {
             // We're sending a separate broadcast for the calling admin with the result.
             if (admin.equals(callingAdmin)) {
                 continue;
@@ -1152,7 +1181,7 @@
                     try {
                         if (packageManager.getPackageInfo(packageName, 0, userId) == null
                                 || packageManager.getActivityInfo(
-                                        policies.get(admin).getValue(), 0, userId) == null) {
+                                policies.get(admin).getValue(), 0, userId) == null) {
                             Slogf.e(TAG, String.format(
                                     "Persistent preferred activity in package %s not found for "
                                             + "user %d, removing policy for admin",
@@ -1450,6 +1479,97 @@
         return false;
     }
 
+    /**
+     * Calculate the size of a policy in bytes
+     */
+
+    private static <V> int sizeOf(PolicyValue<V> value) {
+        try {
+            Parcel parcel = Parcel.obtain();
+            parcel.writeParcelable(value, /* flags= */ 0);
+
+            parcel.setDataPosition(0);
+
+            byte[] bytes;
+
+            bytes = parcel.marshall();
+            return bytes.length;
+        } catch (Exception e) {
+            Log.e(TAG, "Error calculating size of policy: " + e);
+            return 0;
+        }
+    }
+
+    /**
+     * Checks if the policy already exists and removes the current size to prevent recording the
+     * same policy twice.
+     *
+     * Checks if the new sum of the size of all policies is less than the maximum sum of policies
+     * size per admin and returns true.
+     *
+     * If the policy size limit is reached then send policy result to admin and return false.
+     */
+
+    private <V> boolean handleAdminPolicySizeLimit(PolicyState<V> policyState, EnforcingAdmin admin,
+            PolicyValue<V> value, PolicyDefinition policyDefinition, int userId) {
+        int currentSize = 0;
+        if (mAdminPolicySize.contains(admin.getUserId())
+                && mAdminPolicySize.get(
+                admin.getUserId()).containsKey(admin)) {
+            currentSize = mAdminPolicySize.get(admin.getUserId()).get(admin);
+        }
+        if (policyState.getPoliciesSetByAdmins().containsKey(admin)) {
+            currentSize -= sizeOf(policyState.getPoliciesSetByAdmins().get(admin));
+        }
+        int policySize = sizeOf(value);
+        if (currentSize + policySize < POLICY_SIZE_LIMIT) {
+            increasePolicySizeForAdmin(admin, policySize);
+            return true;
+        } else {
+            sendPolicyResultToAdmin(
+                    admin,
+                    policyDefinition,
+                    RESULT_FAILURE_STORAGE_LIMIT_REACHED,
+                    userId);
+            return false;
+        }
+    }
+
+    /**
+     * Increase the int in mAdminPolicySize representing the size of the sum of all
+     * active policies for that admin.
+     */
+
+    private <V> void increasePolicySizeForAdmin(EnforcingAdmin admin, int policySize) {
+        if (!mAdminPolicySize.contains(admin.getUserId())) {
+            mAdminPolicySize.put(admin.getUserId(), new HashMap<>());
+        }
+        if (!mAdminPolicySize.get(admin.getUserId()).containsKey(admin)) {
+            mAdminPolicySize.get(admin.getUserId()).put(admin, /* size= */ 0);
+        }
+        mAdminPolicySize.get(admin.getUserId()).put(admin,
+                mAdminPolicySize.get(admin.getUserId()).get(admin) + policySize);
+    }
+
+    /**
+     * Decrease the int in mAdminPolicySize representing the size of the sum of all
+     * active policies for that admin.
+     */
+
+    private <V> void decreasePolicySizeForAdmin(PolicyState<V> policyState, EnforcingAdmin admin) {
+        if (policyState.getPoliciesSetByAdmins().containsKey(admin)) {
+            mAdminPolicySize.get(admin.getUserId()).put(admin,
+                    mAdminPolicySize.get(admin.getUserId()).get(admin) - sizeOf(
+                            policyState.getPoliciesSetByAdmins().get(admin)));
+        }
+        if (mAdminPolicySize.get(admin.getUserId()).get(admin) <= 0) {
+            mAdminPolicySize.get(admin.getUserId()).remove(admin);
+        }
+        if (mAdminPolicySize.get(admin.getUserId()).isEmpty()) {
+            mAdminPolicySize.remove(admin.getUserId());
+        }
+    }
+
     @NonNull
     private Set<EnforcingAdmin> getEnforcingAdminsOnUser(int userId) {
         synchronized (mLock) {
@@ -1508,11 +1628,13 @@
         clear();
         write();
     }
+
     private void clear() {
         synchronized (mLock) {
             mGlobalPolicies.clear();
             mLocalPolicies.clear();
             mEnforcingAdmins.clear();
+            mAdminPolicySize.clear();
         }
     }
 
@@ -1553,7 +1675,11 @@
         private static final String TAG_POLICY_STATE_ENTRY = "policy-state-entry";
         private static final String TAG_POLICY_KEY_ENTRY = "policy-key-entry";
         private static final String TAG_ENFORCING_ADMINS_ENTRY = "enforcing-admins-entry";
+        private static final String TAG_ENFORCING_ADMIN_AND_SIZE = "enforcing-admin-and-size";
+        private static final String TAG_ENFORCING_ADMIN = "enforcing-admin";
+        private static final String TAG_POLICY_SUM_SIZE = "policy-sum-size";
         private static final String ATTR_USER_ID = "user-id";
+        private static final String ATTR_POLICY_SUM_SIZE = "size";
 
         private final File mFile;
 
@@ -1595,6 +1721,7 @@
             writeLocalPoliciesInner(serializer);
             writeGlobalPoliciesInner(serializer);
             writeEnforcingAdminsInner(serializer);
+            writeEnforcingAdminSizeInner(serializer);
         }
 
         private void writeLocalPoliciesInner(TypedXmlSerializer serializer) throws IOException {
@@ -1652,6 +1779,30 @@
             }
         }
 
+        private void writeEnforcingAdminSizeInner(TypedXmlSerializer serializer)
+                throws IOException {
+            if (FlagUtils.isDevicePolicySizeTrackingEnabled()) {
+                if (mAdminPolicySize != null) {
+                    for (int i = 0; i < mAdminPolicySize.size(); i++) {
+                        int userId = mAdminPolicySize.keyAt(i);
+                        for (EnforcingAdmin admin : mAdminPolicySize.get(
+                                userId).keySet()) {
+                            serializer.startTag(/* namespace= */ null,
+                                    TAG_ENFORCING_ADMIN_AND_SIZE);
+                            serializer.startTag(/* namespace= */ null, TAG_ENFORCING_ADMIN);
+                            admin.saveToXml(serializer);
+                            serializer.endTag(/* namespace= */ null, TAG_ENFORCING_ADMIN);
+                            serializer.startTag(/* namespace= */ null, TAG_POLICY_SUM_SIZE);
+                            serializer.attributeInt(/* namespace= */ null, ATTR_POLICY_SUM_SIZE,
+                                    mAdminPolicySize.get(userId).get(admin));
+                            serializer.endTag(/* namespace= */ null, TAG_POLICY_SUM_SIZE);
+                            serializer.endTag(/* namespace= */ null, TAG_ENFORCING_ADMIN_AND_SIZE);
+                        }
+                    }
+                }
+            }
+        }
+
         void readFromFileLocked() {
             if (!mFile.exists()) {
                 Log.d(TAG, "" + mFile + " doesn't exist");
@@ -1689,6 +1840,9 @@
                     case TAG_ENFORCING_ADMINS_ENTRY:
                         readEnforcingAdminsInner(parser);
                         break;
+                    case TAG_ENFORCING_ADMIN_AND_SIZE:
+                        readEnforcingAdminAndSizeInner(parser);
+                        break;
                     default:
                         Slogf.wtf(TAG, "Unknown tag " + tag);
                 }
@@ -1767,5 +1921,37 @@
             }
             mEnforcingAdmins.get(admin.getUserId()).add(admin);
         }
+
+        private void readEnforcingAdminAndSizeInner(TypedXmlPullParser parser)
+                throws XmlPullParserException, IOException {
+            int outerDepth = parser.getDepth();
+            EnforcingAdmin admin = null;
+            int size = 0;
+            while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+                String tag = parser.getName();
+                switch (tag) {
+                    case TAG_ENFORCING_ADMIN:
+                        admin = EnforcingAdmin.readFromXml(parser);
+                        break;
+                    case TAG_POLICY_SUM_SIZE:
+                        size = parser.getAttributeInt(/* namespace= */ null, ATTR_POLICY_SUM_SIZE);
+                        break;
+                    default:
+                        Slogf.wtf(TAG, "Unknown tag " + tag);
+                }
+            }
+            if (admin == null) {
+                Slogf.wtf(TAG, "Error parsing enforcingAdmins, EnforcingAdmin is null.");
+                return;
+            }
+            if (size <= 0) {
+                Slogf.wtf(TAG, "Error parsing policy size, size is " + size);
+                return;
+            }
+            if (!mAdminPolicySize.contains(admin.getUserId())) {
+                mAdminPolicySize.put(admin.getUserId(), new HashMap<>());
+            }
+            mAdminPolicySize.get(admin.getUserId()).put(admin, size);
+        }
     }
 }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index f604932..6aa135a 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -241,7 +241,6 @@
 import static android.provider.Telephony.Carriers.ENFORCE_MANAGED_URI;
 import static android.provider.Telephony.Carriers.INVALID_APN_ID;
 import static android.security.keystore.AttestationUtils.USE_INDIVIDUAL_ATTESTATION;
-
 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.PROVISIONING_ENTRY_POINT_ADB;
 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW;
@@ -23455,7 +23454,6 @@
     public DevicePolicyState getDevicePolicyState() {
         Preconditions.checkCallAuthorization(
                 hasCallingOrSelfPermission(MANAGE_PROFILE_AND_DEVICE_OWNERS));
-
         return mInjector.binderWithCleanCallingIdentity(mDevicePolicyEngine::getDevicePolicyState);
     }
 
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/flags/FlagUtils.java b/services/devicepolicy/java/com/android/server/devicepolicy/flags/FlagUtils.java
index 9fe3749..7e17ef11 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/flags/FlagUtils.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/flags/FlagUtils.java
@@ -16,6 +16,7 @@
 
 package com.android.server.devicepolicy.flags;
 
+import static com.android.server.devicepolicy.flags.Flags.devicePolicySizeTrackingEnabled;
 import static com.android.server.devicepolicy.flags.Flags.policyEngineMigrationV2Enabled;
 
 import android.os.Binder;
@@ -28,4 +29,10 @@
             return policyEngineMigrationV2Enabled();
         });
     }
+
+    public static boolean isDevicePolicySizeTrackingEnabled() {
+        return Binder.withCleanCallingIdentity(() -> {
+            return devicePolicySizeTrackingEnabled();
+        });
+    }
 }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/flags/flags.aconfig b/services/devicepolicy/java/com/android/server/devicepolicy/flags/flags.aconfig
index 00702a9..0dde496 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/flags/flags.aconfig
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/flags/flags.aconfig
@@ -5,4 +5,10 @@
   namespace: "enterprise"
   description: "V2 of the policy engine migrations for Android V"
   bug: "289520697"
+}
+flag {
+  name: "device_policy_size_tracking_enabled"
+  namespace: "enterprise"
+  description: "Add feature to track the total policy size and have a max threshold."
+  bug: "281543351"
 }
\ No newline at end of file
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerServiceTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerServiceTest.java
index 4a2bf75..5d3eba8 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerServiceTest.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerServiceTest.java
@@ -17,24 +17,22 @@
 package com.android.server.pm;
 
 import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
+
 import static com.google.common.truth.Truth.assertWithMessage;
+
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.fail;
+
 import static java.lang.reflect.Modifier.isFinal;
 import static java.lang.reflect.Modifier.isPublic;
 import static java.lang.reflect.Modifier.isStatic;
 
-import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.app.AppGlobals;
-import android.content.IIntentReceiver;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
-import android.os.Bundle;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.platform.test.annotations.Postsubmit;
-import android.util.SparseArray;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
@@ -87,18 +85,6 @@
     @Test
     public void testPackageRemoval() {
         class PackageSenderImpl implements PackageSender {
-            public void sendPackageBroadcast(final String action, final String pkg,
-                    final Bundle extras, final int flags, final String targetPkg,
-                    final IIntentReceiver finishedReceiver, final int[] userIds,
-                    int[] instantUserIds, SparseArray<int[]> broadcastAllowList,
-                    @Nullable Bundle bOptions) {
-            }
-
-            public void sendPackageAddedForNewUsers(@NonNull Computer snapshot, String packageName,
-                    boolean sendBootComplete, boolean includeStopped, int appId,
-                    int[] userIds, int[] instantUserIds, boolean isArchived, int dataLoaderType) {
-            }
-
             @Override
             public void notifyPackageAdded(String packageName, int uid) {
             }
@@ -113,9 +99,8 @@
             }
         }
 
-        PackageSenderImpl sender = new PackageSenderImpl();
         PackageSetting setting = null;
-        PackageRemovedInfo pri = new PackageRemovedInfo(sender);
+        PackageRemovedInfo pri = new PackageRemovedInfo();
 
         // Initial conditions: nothing there
         Assert.assertNull(pri.mRemovedUsers);
diff --git a/services/tests/displayservicetests/src/com/android/server/display/BrightnessSynchronizerTest.java b/services/tests/displayservicetests/src/com/android/server/display/BrightnessSynchronizerTest.java
index 2fd6e5f..7a4327c 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/BrightnessSynchronizerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/BrightnessSynchronizerTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.isA;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -27,15 +28,18 @@
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.database.ContentObserver;
+import android.hardware.display.BrightnessInfo;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManager.DisplayListener;
 import android.net.Uri;
 import android.os.Handler;
+import android.os.PowerManager;
 import android.os.UserHandle;
 import android.os.test.TestLooper;
 import android.provider.Settings;
 import android.test.mock.MockContentResolver;
 import android.view.Display;
+import android.view.DisplayAdjustments;
 
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.filters.SmallTest;
@@ -59,6 +63,7 @@
     private static final float EPSILON = 0.00001f;
     private static final Uri BRIGHTNESS_URI =
             Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS);
+    private static final float BRIGHTNESS_MAX = 0.6f;
 
     private Context mContext;
     private MockContentResolver mContentResolverSpy;
@@ -66,6 +71,7 @@
     private DisplayListener mDisplayListener;
     private ContentObserver mContentObserver;
     private TestLooper mTestLooper;
+    private BrightnessSynchronizer mSynchronizer;
 
     @Mock private DisplayManager mDisplayManagerMock;
     @Captor private ArgumentCaptor<DisplayListener> mDisplayListenerCaptor;
@@ -74,7 +80,17 @@
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        mContext = spy(new ContextWrapper(ApplicationProvider.getApplicationContext()));
+
+        Display display = mock(Display.class);
+        when(display.getDisplayAdjustments()).thenReturn(new DisplayAdjustments());
+        BrightnessInfo info = new BrightnessInfo(PowerManager.BRIGHTNESS_INVALID_FLOAT,
+                PowerManager.BRIGHTNESS_MIN, BRIGHTNESS_MAX,
+                BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF, BRIGHTNESS_MAX,
+                BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE);
+        when(display.getBrightnessInfo()).thenReturn(info);
+
+        mContext = spy(new ContextWrapper(
+                ApplicationProvider.getApplicationContext().createDisplayContext(display)));
         mContentResolverSpy = spy(new MockContentResolver(mContext));
         mContentResolverSpy.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
         when(mContext.getContentResolver()).thenReturn(mContentResolverSpy);
@@ -128,13 +144,12 @@
     @Test
     public void testSetSameIntValue_nothingUpdated() {
         putFloatSetting(0.5f);
-        putIntSetting(128);
         start();
 
-        putIntSetting(128);
+        putIntSetting(fToI(0.5f));
         advanceTime(10);
         verify(mDisplayManagerMock, times(0)).setBrightness(
-                eq(Display.DEFAULT_DISPLAY), eq(iToF(128)));
+                eq(Display.DEFAULT_DISPLAY), eq(0.5f));
     }
 
     @Test
@@ -154,14 +169,13 @@
         // Verify that this update did not get sent to float, because synchronizer
         // is still waiting for confirmation of its first value.
         verify(mDisplayManagerMock, times(0)).setBrightness(
-                eq(Display.DEFAULT_DISPLAY), eq(iToF(20)));
+                Display.DEFAULT_DISPLAY, iToF(20));
 
         // Send the confirmation of the initial change. This should trigger the new value to
         // finally be processed and we can verify that the new value (20) is sent.
         putIntSetting(fToI(0.4f));
         advanceTime(10);
-        verify(mDisplayManagerMock).setBrightness(
-                eq(Display.DEFAULT_DISPLAY), eq(iToF(20)));
+        verify(mDisplayManagerMock).setBrightness(Display.DEFAULT_DISPLAY, iToF(20));
 
     }
 
@@ -183,8 +197,7 @@
         advanceTime(200);
 
         // Verify that the new value gets sent because the timeout expired.
-        verify(mDisplayManagerMock).setBrightness(
-                eq(Display.DEFAULT_DISPLAY), eq(iToF(20)));
+        verify(mDisplayManagerMock).setBrightness(Display.DEFAULT_DISPLAY, iToF(20));
 
         // Send a confirmation of the initial event, BrightnessSynchronizer should treat this as a
         // new event because the timeout had already expired
@@ -196,14 +209,14 @@
 
         // Verify we sent what would have been the confirmation as a new event to displaymanager.
         // We do both fToI and iToF because the conversions are not symmetric.
-        verify(mDisplayManagerMock).setBrightness(
-                eq(Display.DEFAULT_DISPLAY), eq(iToF(fToI(0.4f))));
+        verify(mDisplayManagerMock).setBrightness(Display.DEFAULT_DISPLAY,
+                iToF(fToI(0.4f)));
     }
 
-    private BrightnessSynchronizer start() {
-        BrightnessSynchronizer bs = new BrightnessSynchronizer(mContext, mTestLooper.getLooper(),
+    private void start() {
+        mSynchronizer = new BrightnessSynchronizer(mContext, mTestLooper.getLooper(),
                 mClock::now);
-        bs.startSynchronizing();
+        mSynchronizer.startSynchronizing();
         verify(mDisplayManagerMock).registerDisplayListener(mDisplayListenerCaptor.capture(),
                 isA(Handler.class), eq(DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS));
         mDisplayListener = mDisplayListenerCaptor.getValue();
@@ -211,7 +224,6 @@
         verify(mContentResolverSpy).registerContentObserver(eq(BRIGHTNESS_URI), eq(false),
                 mContentObserverCaptor.capture(), eq(UserHandle.USER_ALL));
         mContentObserver = mContentObserverCaptor.getValue();
-        return bs;
     }
 
     private int getIntSetting() throws Exception {
@@ -241,11 +253,11 @@
     }
 
     private int fToI(float brightness) {
-        return BrightnessSynchronizer.brightnessFloatToInt(brightness);
+        return mSynchronizer.brightnessFloatToIntSetting(brightness);
     }
 
     private float iToF(int brightness) {
-        return BrightnessSynchronizer.brightnessIntToFloat(brightness);
+        return mSynchronizer.brightnessIntSettingToFloat(brightness);
     }
 
     private void advanceTime(long timeMs) {
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
index a23539e..306de52 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -71,10 +71,12 @@
 import android.hardware.Sensor;
 import android.hardware.SensorManager;
 import android.hardware.display.BrightnessConfiguration;
+import android.hardware.display.BrightnessInfo;
 import android.hardware.display.Curve;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManagerGlobal;
 import android.hardware.display.DisplayManagerInternal;
+import android.hardware.display.DisplayManagerInternal.DisplayOffloader;
 import android.hardware.display.DisplayViewport;
 import android.hardware.display.DisplayedContentSample;
 import android.hardware.display.DisplayedContentSamplingAttributes;
@@ -87,11 +89,13 @@
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.Looper;
 import android.os.MessageQueue;
 import android.os.Process;
 import android.os.RemoteException;
 import android.view.ContentRecordingSession;
 import android.view.Display;
+import android.view.DisplayAdjustments;
 import android.view.DisplayCutout;
 import android.view.DisplayEventReceiver;
 import android.view.DisplayInfo;
@@ -99,7 +103,6 @@
 import android.view.SurfaceControl;
 import android.window.DisplayWindowPolicyController;
 
-import androidx.test.InstrumentationRegistry;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.filters.FlakyTest;
 import androidx.test.filters.SmallTest;
@@ -118,11 +121,11 @@
 import com.android.server.sensors.SensorManagerInternal;
 import com.android.server.wm.WindowManagerInternal;
 
-import com.google.common.truth.Expect;
-
 import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
 import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
 
+import com.google.common.truth.Expect;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -198,9 +201,10 @@
 
                 @Override
                 LocalDisplayAdapter getLocalDisplayAdapter(SyncRoot syncRoot, Context context,
-                        Handler handler, DisplayAdapter.Listener displayAdapterListener) {
+                        Handler handler, DisplayAdapter.Listener displayAdapterListener,
+                        DisplayManagerFlags flags) {
                     return new LocalDisplayAdapter(syncRoot, context, handler,
-                            displayAdapterListener, new LocalDisplayAdapter.Injector() {
+                            displayAdapterListener, flags, new LocalDisplayAdapter.Injector() {
                         @Override
                         public LocalDisplayAdapter.SurfaceControlProxy getSurfaceControlProxy() {
                             return mSurfaceControlProxy;
@@ -244,8 +248,14 @@
 
         @Override
         LocalDisplayAdapter getLocalDisplayAdapter(SyncRoot syncRoot, Context context,
-                Handler handler, DisplayAdapter.Listener displayAdapterListener) {
-            return new LocalDisplayAdapter(syncRoot, context, handler, displayAdapterListener,
+                Handler handler, DisplayAdapter.Listener displayAdapterListener,
+                DisplayManagerFlags flags) {
+            return new LocalDisplayAdapter(
+                    syncRoot,
+                    context,
+                    handler,
+                    displayAdapterListener,
+                    flags,
                     new LocalDisplayAdapter.Injector() {
                         @Override
                         public LocalDisplayAdapter.SurfaceControlProxy getSurfaceControlProxy() {
@@ -323,7 +333,11 @@
         LocalServices.removeServiceForTest(UserManagerInternal.class);
         LocalServices.addService(UserManagerInternal.class, mMockUserManagerInternal);
         // TODO: b/287945043
-        mContext = spy(new ContextWrapper(ApplicationProvider.getApplicationContext()));
+        Display display = mock(Display.class);
+        when(display.getDisplayAdjustments()).thenReturn(new DisplayAdjustments());
+        when(display.getBrightnessInfo()).thenReturn(mock(BrightnessInfo.class));
+        mContext = spy(new ContextWrapper(
+                ApplicationProvider.getApplicationContext().createDisplayContext(display)));
         mResources = Mockito.spy(mContext.getResources());
         manageDisplaysPermission(/* granted= */ false);
         when(mContext.getResources()).thenReturn(mResources);
@@ -1898,7 +1912,6 @@
 
     @Test
     public void testSettingTwoBrightnessConfigurationsOnMultiDisplay() {
-        Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
         DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
 
         // get the first two internal displays
@@ -2445,6 +2458,86 @@
         assertThat(callback.receivedEvents()).containsExactly(EVENT_DISPLAY_REMOVED,
                 EVENT_DISPLAY_DISCONNECTED);
     }
+
+    @Test
+    public void testRegisterDisplayOffloader_whenEnabled_DisplayHasDisplayOffloadSession() {
+        when(mMockFlags.isDisplayOffloadEnabled()).thenReturn(true);
+        // set up DisplayManager
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerInternal localService = displayManager.new LocalService();
+        // set up display
+        FakeDisplayDevice displayDevice =
+                createFakeDisplayDevice(displayManager, new float[]{60f}, Display.DEFAULT_DISPLAY);
+        initDisplayPowerController(localService);
+        LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
+        LogicalDisplay display =
+                logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true);
+        int displayId = display.getDisplayIdLocked();
+
+        // Register DisplayOffloader.
+        DisplayOffloader mockDisplayOffloader = mock(DisplayOffloader.class);
+        localService.registerDisplayOffloader(displayId, mockDisplayOffloader);
+
+        assertThat(display.getDisplayOffloadSessionLocked().getDisplayOffloader()).isEqualTo(
+                mockDisplayOffloader);
+    }
+
+    @Test
+    public void testRegisterDisplayOffloader_whenDisabled_DisplayHasNoDisplayOffloadSession() {
+        when(mMockFlags.isDisplayOffloadEnabled()).thenReturn(false);
+        // set up DisplayManager
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerInternal localService = displayManager.new LocalService();
+        // set up display
+        FakeDisplayDevice displayDevice =
+                createFakeDisplayDevice(displayManager, new float[]{60f}, Display.DEFAULT_DISPLAY);
+        initDisplayPowerController(localService);
+        LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
+        LogicalDisplay display =
+                logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true);
+        int displayId = display.getDisplayIdLocked();
+
+        // Register DisplayOffloader.
+        DisplayOffloader mockDisplayOffloader = mock(DisplayOffloader.class);
+        localService.registerDisplayOffloader(displayId, mockDisplayOffloader);
+
+        assertThat(display.getDisplayOffloadSessionLocked()).isNull();
+    }
+
+    private void initDisplayPowerController(DisplayManagerInternal localService) {
+        localService.initPowerManagement(new DisplayManagerInternal.DisplayPowerCallbacks() {
+            @Override
+            public void onStateChanged() {
+
+            }
+
+            @Override
+            public void onProximityPositive() {
+
+            }
+
+            @Override
+            public void onProximityNegative() {
+
+            }
+
+            @Override
+            public void onDisplayStateChange(boolean allInactive, boolean allOff) {
+
+            }
+
+            @Override
+            public void acquireSuspendBlocker(String id) {
+
+            }
+
+            @Override
+            public void releaseSuspendBlocker(String id) {
+
+            }
+        }, new Handler(Looper.getMainLooper()), mSensorManager);
+    }
+
     private void testDisplayInfoFrameRateOverrideModeCompat(boolean compatChangeEnabled) {
         DisplayManagerService displayManager =
                 new DisplayManagerService(mContext, mShortMockedInjector);
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
index a56b59a..dca69eb 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
@@ -44,6 +44,7 @@
 import android.hardware.SensorEventListener;
 import android.hardware.SensorManager;
 import android.hardware.display.BrightnessInfo;
+import android.hardware.display.DisplayManagerInternal;
 import android.hardware.display.DisplayManagerInternal.DisplayPowerCallbacks;
 import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
 import android.os.Handler;
@@ -123,6 +124,9 @@
     private Handler mHandler;
     private DisplayPowerControllerHolder mHolder;
     private Sensor mProxSensor;
+    private DisplayManagerInternal.DisplayOffloader mDisplayOffloader;
+    private DisplayManagerInternal.DisplayOffloadSession mDisplayOffloadSession;
+
     @Mock
     private DisplayPowerCallbacks mDisplayPowerCallbacksMock;
     @Mock
@@ -1409,6 +1413,111 @@
                 BRIGHTNESS_RAMP_DECREASE_MAX_IDLE);
     }
 
+    @Test
+    public void testDozeScreenStateOverride_toSupportedOffloadStateFromDoze_DisplayStateChanges() {
+        // set up.
+        int initState = Display.STATE_DOZE;
+        int supportedTargetState = Display.STATE_DOZE_SUSPEND;
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        doAnswer(invocation -> {
+            when(mHolder.displayPowerState.getScreenState()).thenReturn(invocation.getArgument(0));
+            return null;
+        }).when(mHolder.displayPowerState).setScreenState(anyInt());
+        // init displayoffload session and support offloading.
+        initDisplayOffloadSession();
+        mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
+
+        // start with DOZE.
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(initState);
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        mHolder.dpc.overrideDozeScreenState(supportedTargetState);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.displayPowerState).setScreenState(supportedTargetState);
+    }
+
+    @Test
+    public void testDozeScreenStateOverride_toUnSupportedOffloadStateFromDoze_stateRemains() {
+        // set up.
+        int initState = Display.STATE_DOZE;
+        int unSupportedTargetState = Display.STATE_ON;
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        doAnswer(invocation -> {
+            when(mHolder.displayPowerState.getScreenState()).thenReturn(invocation.getArgument(0));
+            return null;
+        }).when(mHolder.displayPowerState).setScreenState(anyInt());
+        // init displayoffload session and support offloading.
+        initDisplayOffloadSession();
+        mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
+
+        // start with DOZE.
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(initState);
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        mHolder.dpc.overrideDozeScreenState(unSupportedTargetState);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.displayPowerState, never()).setScreenState(anyInt());
+    }
+
+    @Test
+    public void testDozeScreenStateOverride_toSupportedOffloadStateFromOFF_stateRemains() {
+        // set up.
+        int initState = Display.STATE_OFF;
+        int supportedTargetState = Display.STATE_DOZE_SUSPEND;
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
+        doAnswer(invocation -> {
+            when(mHolder.displayPowerState.getScreenState()).thenReturn(invocation.getArgument(0));
+            return null;
+        }).when(mHolder.displayPowerState).setScreenState(anyInt());
+        // init displayoffload session and support offloading.
+        initDisplayOffloadSession();
+        mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
+
+        // start with OFF.
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(initState);
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_OFF;
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        mHolder.dpc.overrideDozeScreenState(supportedTargetState);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.displayPowerState, never()).setScreenState(anyInt());
+    }
+
+    private void initDisplayOffloadSession() {
+        mDisplayOffloader = spy(new DisplayManagerInternal.DisplayOffloader() {
+            @Override
+            public boolean startOffload() {
+                return true;
+            }
+
+            @Override
+            public void stopOffload() {}
+        });
+
+        mDisplayOffloadSession = new DisplayManagerInternal.DisplayOffloadSession() {
+            @Override
+            public void setDozeStateOverride(int displayState) {}
+
+            @Override
+            public DisplayManagerInternal.DisplayOffloader getDisplayOffloader() {
+                return mDisplayOffloader;
+            }
+        };
+    }
+
     /**
      * Creates a mock and registers it to {@link LocalServices}.
      */
@@ -1742,9 +1851,9 @@
         BrightnessRangeController getBrightnessRangeController(
                 HighBrightnessModeController hbmController, Runnable modeChangeCallback,
                 DisplayDeviceConfig displayDeviceConfig, Handler handler,
-                DisplayManagerFlags flags) {
+                DisplayManagerFlags flags, IBinder displayToken, DisplayDeviceInfo info) {
             return new BrightnessRangeController(hbmController, modeChangeCallback,
-                    displayDeviceConfig, mHdrClamper, mFlags);
+                    displayDeviceConfig, mHdrClamper, mFlags, displayToken, info);
         }
 
         @Override
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index 0572117..edaa1d5 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -44,6 +44,7 @@
 import android.hardware.SensorEventListener;
 import android.hardware.SensorManager;
 import android.hardware.display.BrightnessInfo;
+import android.hardware.display.DisplayManagerInternal;
 import android.hardware.display.DisplayManagerInternal.DisplayPowerCallbacks;
 import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
 import android.os.Handler;
@@ -122,6 +123,8 @@
     private Handler mHandler;
     private DisplayPowerControllerHolder mHolder;
     private Sensor mProxSensor;
+    private DisplayManagerInternal.DisplayOffloader mDisplayOffloader;
+    private DisplayManagerInternal.DisplayOffloadSession mDisplayOffloadSession;
 
     @Mock
     private DisplayPowerCallbacks mDisplayPowerCallbacksMock;
@@ -1364,6 +1367,110 @@
         verify(mHolder.animator).setAnimationTimeLimits(BRIGHTNESS_RAMP_INCREASE_MAX_IDLE,
                 BRIGHTNESS_RAMP_DECREASE_MAX_IDLE);
     }
+    @Test
+    public void testDozeScreenStateOverride_toSupportedOffloadStateFromDoze_DisplayStateChanges() {
+        // set up.
+        int initState = Display.STATE_DOZE;
+        int supportedTargetState = Display.STATE_DOZE_SUSPEND;
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        doAnswer(invocation -> {
+            when(mHolder.displayPowerState.getScreenState()).thenReturn(invocation.getArgument(0));
+            return null;
+        }).when(mHolder.displayPowerState).setScreenState(anyInt());
+        // init displayoffload session and support offloading.
+        initDisplayOffloadSession();
+        mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
+
+        // start with DOZE.
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(initState);
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        mHolder.dpc.overrideDozeScreenState(supportedTargetState);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.displayPowerState).setScreenState(supportedTargetState);
+    }
+
+    @Test
+    public void testDozeScreenStateOverride_toUnSupportedOffloadStateFromDoze_stateRemains() {
+        // set up.
+        int initState = Display.STATE_DOZE;
+        int unSupportedTargetState = Display.STATE_ON;
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        doAnswer(invocation -> {
+            when(mHolder.displayPowerState.getScreenState()).thenReturn(invocation.getArgument(0));
+            return null;
+        }).when(mHolder.displayPowerState).setScreenState(anyInt());
+        // init displayoffload session and support offloading.
+        initDisplayOffloadSession();
+        mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
+
+        // start with DOZE.
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(initState);
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        mHolder.dpc.overrideDozeScreenState(unSupportedTargetState);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.displayPowerState, never()).setScreenState(anyInt());
+    }
+
+    @Test
+    public void testDozeScreenStateOverride_toSupportedOffloadStateFromOFF_stateRemains() {
+        // set up.
+        int initState = Display.STATE_OFF;
+        int supportedTargetState = Display.STATE_DOZE_SUSPEND;
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
+        doAnswer(invocation -> {
+            when(mHolder.displayPowerState.getScreenState()).thenReturn(invocation.getArgument(0));
+            return null;
+        }).when(mHolder.displayPowerState).setScreenState(anyInt());
+        // init displayoffload session and support offloading.
+        initDisplayOffloadSession();
+        mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
+
+        // start with OFF.
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(initState);
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_OFF;
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        mHolder.dpc.overrideDozeScreenState(supportedTargetState);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.displayPowerState, never()).setScreenState(anyInt());
+    }
+
+    private void initDisplayOffloadSession() {
+        mDisplayOffloader = spy(new DisplayManagerInternal.DisplayOffloader() {
+            @Override
+            public boolean startOffload() {
+                return true;
+            }
+
+            @Override
+            public void stopOffload() {}
+        });
+
+        mDisplayOffloadSession = new DisplayManagerInternal.DisplayOffloadSession() {
+            @Override
+            public void setDozeStateOverride(int displayState) {}
+
+            @Override
+            public DisplayManagerInternal.DisplayOffloader getDisplayOffloader() {
+                return mDisplayOffloader;
+            }
+        };
+    }
 
     private void advanceTime(long timeMs) {
         mClock.fastForward(timeMs);
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
index 6cde5e3..147e8f2 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
@@ -32,12 +32,15 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.graphics.Rect;
+import android.hardware.display.DisplayManagerInternal.DisplayOffloadSession;
+import android.hardware.display.DisplayManagerInternal.DisplayOffloader;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
@@ -55,6 +58,7 @@
 import com.android.internal.R;
 import com.android.server.LocalServices;
 import com.android.server.display.LocalDisplayAdapter.BacklightAdapter;
+import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.mode.DisplayModeDirector;
 import com.android.server.lights.LightsManager;
 import com.android.server.lights.LogicalLight;
@@ -72,6 +76,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.LinkedList;
+import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -107,8 +112,15 @@
     private LightsManager mMockedLightsManager;
     @Mock
     private LogicalLight mMockedBacklight;
+    @Mock
+    private DisplayManagerFlags mFlags;
+
     private Handler mHandler;
 
+    private DisplayOffloadSession mDisplayOffloadSession;
+
+    private DisplayOffloader mDisplayOffloader;
+
     private TestListener mListener = new TestListener();
 
     private LinkedList<DisplayAddress.Physical> mAddresses = new LinkedList<>();
@@ -120,6 +132,8 @@
     private static final float[] DISPLAY_RANGE_NITS = { 2.685f, 478.5f };
     private static final int[] BACKLIGHT_RANGE = { 1, 255 };
     private static final float[] BACKLIGHT_RANGE_ZERO_TO_ONE = { 0.0f, 1.0f };
+    private static final List<Integer> mDisplayOffloadSupportedStates
+            = new ArrayList<>(List.of(Display.STATE_DOZE_SUSPEND));
 
     @Before
     public void setUp() throws Exception {
@@ -134,7 +148,7 @@
         mInjector = new Injector();
         when(mSurfaceControlProxy.getBootDisplayModeSupport()).thenReturn(true);
         mAdapter = new LocalDisplayAdapter(mMockedSyncRoot, mMockedContext, mHandler,
-                mListener, mInjector);
+                mListener, mFlags, mInjector);
         spyOn(mAdapter);
         doReturn(mMockedContext).when(mAdapter).getOverlayContext();
 
@@ -185,6 +199,8 @@
         when(mMockedResources.getIntArray(
             com.android.internal.R.array.config_highAmbientBrightnessThresholdsOfFixedRefreshRate))
             .thenReturn(new int[]{});
+        doReturn(true).when(mFlags).isDisplayOffloadEnabled();
+        initDisplayOffloadSession();
     }
 
     @After
@@ -1109,6 +1125,72 @@
         assertThat(info.flags & DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP).isEqualTo(0);
     }
 
+
+    @Test
+    public void test_displayStateToSupportedState_DisplayOffloadStart()
+            throws InterruptedException {
+        // prepare a display.
+        FakeDisplay display = new FakeDisplay(PORT_A);
+        setUpDisplay(display);
+        updateAvailableDisplays();
+        mAdapter.registerLocked();
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+        assertThat(mListener.addedDisplays.size()).isEqualTo(1);
+        DisplayDevice displayDevice = mListener.addedDisplays.get(0);
+
+        for (Integer supportedState : mDisplayOffloadSupportedStates) {
+            Runnable changeStateRunnable = displayDevice.requestDisplayStateLocked(
+                    supportedState, 0, 0, mDisplayOffloadSession);
+            changeStateRunnable.run();
+
+            verify(mDisplayOffloader).startOffload();
+        }
+    }
+
+    @Test
+    public void test_displayStateToDozeFromDozeSuspend_DisplayOffloadStop()
+            throws InterruptedException {
+        // prepare a display.
+        FakeDisplay display = new FakeDisplay(PORT_A);
+        setUpDisplay(display);
+        updateAvailableDisplays();
+        mAdapter.registerLocked();
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+        assertThat(mListener.addedDisplays.size()).isEqualTo(1);
+        DisplayDevice displayDevice = mListener.addedDisplays.get(0);
+
+        Runnable changeStateToDozeSuspendRunnable = displayDevice.requestDisplayStateLocked(
+                Display.STATE_DOZE_SUSPEND, 0, 0, mDisplayOffloadSession);
+        Runnable changeStateToDozeRunnable = displayDevice.requestDisplayStateLocked(
+                Display.STATE_DOZE, 0, 0, mDisplayOffloadSession);
+        changeStateToDozeSuspendRunnable.run();
+        changeStateToDozeRunnable.run();
+
+        verify(mDisplayOffloader).stopOffload();
+    }
+
+    private void initDisplayOffloadSession() {
+        mDisplayOffloader = spy(new DisplayOffloader() {
+            @Override
+            public boolean startOffload() {
+                return true;
+            }
+
+            @Override
+            public void stopOffload() {}
+        });
+
+        mDisplayOffloadSession = new DisplayOffloadSession() {
+            @Override
+            public void setDozeStateOverride(int displayState) {}
+
+            @Override
+            public DisplayOffloader getDisplayOffloader() {
+                return mDisplayOffloader;
+            }
+        };
+    }
+
     private void setupCutoutAndRoundedCorners() {
         String sampleCutout = "M 507,66\n"
                 + "a 33,33 0 1 0 66,0 33,33 0 1 0 -66,0\n"
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java
index 37d966d..c3322ec 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java
@@ -20,6 +20,12 @@
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.IBinder;
 import android.os.PowerManager;
 
 import androidx.test.filters.SmallTest;
@@ -31,6 +37,7 @@
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
@@ -40,7 +47,20 @@
 @SmallTest
 public class HdrClamperTest {
 
-    public static final float FLOAT_TOLERANCE = 0.0001f;
+    private static final float FLOAT_TOLERANCE = 0.0001f;
+    private static final long SEND_TIME_TOLERANCE = 100;
+
+    private static final HdrBrightnessData TEST_HDR_DATA = new HdrBrightnessData(
+            Map.of(500f, 0.6f),
+            /* brightnessIncreaseDebounceMillis= */ 1000,
+            /* brightnessIncreaseDurationMillis= */ 2000,
+            /* brightnessDecreaseDebounceMillis= */ 3000,
+            /* brightnessDecreaseDurationMillis= */4000
+    );
+
+    private static final int WIDTH = 600;
+    private static final int HEIGHT = 800;
+    private static final float MIN_HDR_PERCENT = 0.5f;
 
     @Rule
     public MockitoRule mRule = MockitoJUnit.rule();
@@ -48,17 +68,31 @@
     @Mock
     private BrightnessClamperController.ClamperChangeListener mMockListener;
 
+    @Mock
+    private IBinder mMockBinder;
+
+    @Mock
+    private HdrClamper.Injector mMockInjector;
+
+    @Mock
+    private HdrClamper.HdrLayerInfoListener mMockHdrInfoListener;
+
     OffsettableClock mClock = new OffsettableClock.Stopped();
 
     private final TestHandler mTestHandler = new TestHandler(null, mClock);
 
 
     private HdrClamper mHdrClamper;
-
+    private HdrClamper.HdrListener mHdrChangeListener;
 
     @Before
     public void setUp() {
-        mHdrClamper = new HdrClamper(mMockListener, mTestHandler);
+        when(mMockInjector.getHdrListener(any(), any())).thenReturn(mMockHdrInfoListener);
+        mHdrClamper = new HdrClamper(mMockListener, mTestHandler, mMockInjector);
+        ArgumentCaptor<HdrClamper.HdrListener> listenerCaptor = ArgumentCaptor.forClass(
+                HdrClamper.HdrListener.class);
+        verify(mMockInjector).getHdrListener(listenerCaptor.capture(), eq(mTestHandler));
+        mHdrChangeListener = listenerCaptor.getValue();
         configureClamper();
     }
 
@@ -68,20 +102,23 @@
 
         assertFalse(mTestHandler.hasMessagesOrCallbacks());
         assertEquals(PowerManager.BRIGHTNESS_MAX, mHdrClamper.getMaxBrightness(), FLOAT_TOLERANCE);
+        assertEquals(-1, mHdrClamper.getTransitionRate(), FLOAT_TOLERANCE);
     }
 
     @Test
-    public void testClamper_AmbientLuxChangesBelowLimit() {
+    public void testClamper_AmbientLuxChangesBelowLimit_MaxDecrease() {
         mHdrClamper.onAmbientLuxChange(499);
 
         assertTrue(mTestHandler.hasMessagesOrCallbacks());
         TestHandler.MsgInfo msgInfo = mTestHandler.getPendingMessages().peek();
-        assertEquals(2000, msgInfo.sendTime);
+        assertSendTime(3000, msgInfo.sendTime);
         assertEquals(PowerManager.BRIGHTNESS_MAX, mHdrClamper.getMaxBrightness(), FLOAT_TOLERANCE);
+        assertEquals(-1, mHdrClamper.getTransitionRate(), FLOAT_TOLERANCE);
 
-        mClock.fastForward(2000);
+        mClock.fastForward(3000);
         mTestHandler.timeAdvance();
         assertEquals(0.6f, mHdrClamper.getMaxBrightness(), FLOAT_TOLERANCE);
+        assertEquals(0.1f, mHdrClamper.getTransitionRate(), FLOAT_TOLERANCE); // (1-0.6) / 4
     }
 
     @Test
@@ -91,33 +128,65 @@
 
         assertFalse(mTestHandler.hasMessagesOrCallbacks());
         assertEquals(PowerManager.BRIGHTNESS_MAX, mHdrClamper.getMaxBrightness(), FLOAT_TOLERANCE);
+        assertEquals(-1, mHdrClamper.getTransitionRate(), FLOAT_TOLERANCE);
     }
 
     @Test
     public void testClamper_AmbientLuxChangesBelowLimit_ThenSlowlyAboveLimit() {
         mHdrClamper.onAmbientLuxChange(499);
-        mClock.fastForward(2000);
+        mClock.fastForward(3000);
         mTestHandler.timeAdvance();
 
         mHdrClamper.onAmbientLuxChange(500);
 
         assertTrue(mTestHandler.hasMessagesOrCallbacks());
         TestHandler.MsgInfo msgInfo = mTestHandler.getPendingMessages().peek();
-        assertEquals(3000, msgInfo.sendTime); // 2000 + 1000
+        assertSendTime(4000, msgInfo.sendTime); // 3000 + 1000
 
         mClock.fastForward(1000);
         mTestHandler.timeAdvance();
         assertEquals(PowerManager.BRIGHTNESS_MAX, mHdrClamper.getMaxBrightness(), FLOAT_TOLERANCE);
+        assertEquals(0.2f, mHdrClamper.getTransitionRate(), FLOAT_TOLERANCE); // (1-0.6) / 2
+    }
+
+    @Test
+    public void testClamper_HdrOff_ThenAmbientLuxChangesBelowLimit() {
+        mHdrChangeListener.onHdrVisible(false);
+        mHdrClamper.onAmbientLuxChange(499);
+
+        assertFalse(mTestHandler.hasMessagesOrCallbacks());
+        assertEquals(PowerManager.BRIGHTNESS_MAX, mHdrClamper.getMaxBrightness(), FLOAT_TOLERANCE);
+        assertEquals(-1, mHdrClamper.getTransitionRate(), FLOAT_TOLERANCE);
+    }
+
+    @Test
+    public void testClamper_HdrOff_ThenAmbientLuxChangesBelowLimit_ThenHdrOn() {
+        mHdrChangeListener.onHdrVisible(false);
+        mHdrClamper.onAmbientLuxChange(499);
+        mHdrChangeListener.onHdrVisible(true);
+
+        assertTrue(mTestHandler.hasMessagesOrCallbacks());
+        TestHandler.MsgInfo msgInfo = mTestHandler.getPendingMessages().peek();
+        assertSendTime(3000, msgInfo.sendTime);
+        assertEquals(PowerManager.BRIGHTNESS_MAX, mHdrClamper.getMaxBrightness(), FLOAT_TOLERANCE);
+
+        mClock.fastForward(3000);
+        mTestHandler.timeAdvance();
+        assertEquals(0.6f, mHdrClamper.getMaxBrightness(), FLOAT_TOLERANCE);
+        assertEquals(0.1f, mHdrClamper.getTransitionRate(), FLOAT_TOLERANCE);
+    }
+
+    // MsgInfo.sendTime is calculated first by adding SystemClock.uptimeMillis()
+    // (in Handler.sendMessageDelayed) and then by subtracting SystemClock.uptimeMillis()
+    // (in TestHandler.sendMessageAtTime, there might be several milliseconds difference between
+    // SystemClock.uptimeMillis() calls, and subtracted value might be greater than added.
+    private static void assertSendTime(long expectedTime, long sendTime) {
+        assertTrue(expectedTime >= sendTime);
+        assertTrue(expectedTime - SEND_TIME_TOLERANCE < sendTime);
     }
 
     private void configureClamper() {
-        HdrBrightnessData data = new HdrBrightnessData(
-                Map.of(500f, 0.6f),
-                /* brightnessIncreaseDebounceMillis= */ 1000,
-                /* brightnessIncreaseDurationMillis= */ 1500,
-                /* brightnessDecreaseDebounceMillis= */ 2000,
-                /* brightnessDecreaseDurationMillis= */2500
-        );
-        mHdrClamper.resetHdrConfig(data);
+        mHdrClamper.resetHdrConfig(TEST_HDR_DATA, WIDTH, HEIGHT, MIN_HDR_PERCENT, mMockBinder);
+        mHdrChangeListener.onHdrVisible(true);
     }
 }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/state/DisplayStateControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/state/DisplayStateControllerTest.java
index 880501f..f5c6bb2 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/state/DisplayStateControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/state/DisplayStateControllerTest.java
@@ -141,6 +141,32 @@
         assertEquals(false, mDisplayStateController.shouldPerformScreenOffTransition());
     }
 
+    @Test
+    public void dozeScreenStateOverrideToDozeSuspend_DozePolicy_updateDisplayStateToDozeSuspend() {
+        DisplayManagerInternal.DisplayPowerRequest displayPowerRequest =
+                new DisplayManagerInternal.DisplayPowerRequest();
+        displayPowerRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE;
+        mDisplayStateController.overrideDozeScreenState(Display.STATE_DOZE_SUSPEND);
+
+        int state = mDisplayStateController.updateDisplayState(displayPowerRequest, DISPLAY_ENABLED,
+                !DISPLAY_IN_TRANSITION);
+
+        assertEquals(state, Display.STATE_DOZE_SUSPEND);
+    }
+
+    @Test
+    public void dozeScreenStateOverrideToDozeSuspend_OffPolicy_displayRemainOff() {
+        DisplayManagerInternal.DisplayPowerRequest displayPowerRequest =
+                new DisplayManagerInternal.DisplayPowerRequest();
+        displayPowerRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_OFF;
+        mDisplayStateController.overrideDozeScreenState(Display.STATE_DOZE_SUSPEND);
+
+        int state = mDisplayStateController.updateDisplayState(displayPowerRequest, DISPLAY_ENABLED,
+                !DISPLAY_IN_TRANSITION);
+
+        assertEquals(state, Display.STATE_OFF);
+    }
+
     private void validDisplayState(int policy, int displayState, boolean isEnabled,
             boolean isInTransition) {
         DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mock(
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java
index 52044bf..de8b308 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java
@@ -117,7 +117,9 @@
                 Build.VERSION_CODES.CUR_DEVELOPMENT,
                 Build.VERSION.INCREMENTAL);
         mMockSystem.system().validateFinalState();
-        mInstallPackageHelper = new InstallPackageHelper(mPmService, mock(AppDataHelper.class));
+        mInstallPackageHelper = new InstallPackageHelper(mPmService, mock(AppDataHelper.class),
+                mock(RemovePackageHelper.class), mock(DeletePackageHelper.class),
+                mock(BroadcastHelper.class));
     }
 
     @NonNull
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/DeletePackageHelperTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/DeletePackageHelperTest.kt
index d6a4d40..931b38d 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/DeletePackageHelperTest.kt
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/DeletePackageHelperTest.kt
@@ -34,6 +34,7 @@
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
 
 @RunWith(JUnit4::class)
 class DeletePackageHelperTest {
@@ -79,7 +80,8 @@
         whenever(mUserManagerInternal.getUserInfo(1)).thenReturn(UserInfo(1, "test", 0))
         whenever(mUserManagerInternal.getProfileParentId(1)).thenReturn(1)
 
-        val dph = DeletePackageHelper(mPms)
+        val dph = DeletePackageHelper(mPms, mock(RemovePackageHelper::class.java),
+            mock(BroadcastHelper::class.java))
         val result = dph.deletePackageX("a.data.package", 1L, 1, 0, false)
 
         assertThat(result).isEqualTo(PackageManager.DELETE_FAILED_USER_RESTRICTED)
@@ -97,7 +99,8 @@
         whenever(mUserManagerInternal.getUserInfo(parentId)).thenReturn(
             UserInfo(userId, "testparent", 0))
 
-        val dph = DeletePackageHelper(mPms)
+        val dph = DeletePackageHelper(mPms, mock(RemovePackageHelper::class.java),
+            mock(BroadcastHelper::class.java))
         val result = dph.deletePackageX("a.data.package", 1L, userId, 0, false)
 
         assertThat(result).isEqualTo(PackageManager.DELETE_FAILED_USER_RESTRICTED)
@@ -112,7 +115,8 @@
         whenever(mPms.checkPermission(CONTROL_KEYGUARD, "a.data.package", USER_SYSTEM))
             .thenReturn(PERMISSION_DENIED)
 
-        val dph = DeletePackageHelper(mPms)
+        val dph = DeletePackageHelper(mPms, mock(RemovePackageHelper::class.java),
+            mock(BroadcastHelper::class.java))
         val result = dph.deletePackageX("a.data.package", 1L, 1,
             PackageManager.DELETE_SYSTEM_APP, false)
 
@@ -133,7 +137,8 @@
         whenever(mPms.checkPermission(CONTROL_KEYGUARD, "a.data.package", USER_SYSTEM))
             .thenReturn(PERMISSION_DENIED)
 
-        val dph = DeletePackageHelper(mPms)
+        val dph = DeletePackageHelper(mPms, mock(RemovePackageHelper::class.java),
+            mock(BroadcastHelper::class.java))
         val result = dph.deletePackageX("a.data.package", 1L, userId,
             PackageManager.DELETE_SYSTEM_APP, false)
 
@@ -150,7 +155,8 @@
         whenever(mPms.checkPermission(CONTROL_KEYGUARD, "a.data.package", USER_SYSTEM))
             .thenReturn(PERMISSION_DENIED)
 
-        val dph = DeletePackageHelper(mPms)
+        val dph = DeletePackageHelper(mPms, mock(RemovePackageHelper::class.java),
+            mock(BroadcastHelper::class.java))
         val result = dph.deletePackageX("a.data.package", 1L, 1,
                 PackageManager.DELETE_SYSTEM_APP, false)
 
@@ -164,7 +170,8 @@
         whenever(mPms.checkPermission(CONTROL_KEYGUARD, "a.data.package", USER_SYSTEM))
             .thenReturn(PERMISSION_GRANTED)
 
-        val dph = DeletePackageHelper(mPms)
+        val dph = DeletePackageHelper(mPms, mock(RemovePackageHelper::class.java),
+            mock(BroadcastHelper::class.java))
         val result = dph.deletePackageX("a.data.package", 1L, 1,
             PackageManager.DELETE_SYSTEM_APP, false)
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/DistractingPackageHelperTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/DistractingPackageHelperTest.kt
index 9f1cec3..cf81f0a 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/DistractingPackageHelperTest.kt
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/DistractingPackageHelperTest.kt
@@ -39,7 +39,7 @@
     override fun setup() {
         super.setup()
         distractingPackageHelper = DistractingPackageHelper(
-                pms, rule.mocks().injector, broadcastHelper, suspendPackageHelper)
+                pms, broadcastHelper, suspendPackageHelper)
     }
 
     @Test
@@ -50,12 +50,11 @@
         testHandler.flush()
 
         verify(pms).scheduleWritePackageRestrictions(eq(TEST_USER_ID))
-        verify(broadcastHelper).sendPackageBroadcast(eq(Intent.ACTION_DISTRACTING_PACKAGES_CHANGED),
-                nullable(), bundleCaptor.capture(), anyInt(), nullable(), nullable(), any(),
-                nullable(), nullable(), nullable(), nullable())
+        verify(broadcastHelper).sendDistractingPackagesChanged(any(Computer::class.java),
+                pkgListCaptor.capture(), any(), any(), flagsCaptor.capture())
 
-        val modifiedPackages = bundleCaptor.value.getStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST)
-        val distractionFlags = bundleCaptor.value.getInt(Intent.EXTRA_DISTRACTION_RESTRICTIONS)
+        val modifiedPackages = pkgListCaptor.value
+        val distractionFlags = flagsCaptor.value
         assertThat(modifiedPackages).asList().containsExactly(TEST_PACKAGE_1, TEST_PACKAGE_2)
         assertThat(distractionFlags).isEqualTo(PackageManager.RESTRICTION_HIDE_NOTIFICATIONS)
         assertThat(unactionedPackages).isEmpty()
@@ -75,10 +74,8 @@
                 PackageManager.RESTRICTION_HIDE_NOTIFICATIONS, TEST_USER_ID, deviceOwnerUid)
         testHandler.flush()
         verify(pms, never()).scheduleWritePackageRestrictions(eq(TEST_USER_ID))
-        verify(broadcastHelper, never()).sendPackageBroadcast(
-                eq(Intent.ACTION_DISTRACTING_PACKAGES_CHANGED), nullable(), bundleCaptor.capture(),
-                anyInt(), nullable(), nullable(), any(), nullable(), nullable(), nullable(),
-                nullable())
+        verify(broadcastHelper, never()).sendDistractingPackagesChanged(
+                any(), any(), any(), any(), any())
         assertThat(unactionedPackages).isEmpty()
     }
 
@@ -154,11 +151,11 @@
         testHandler.flush()
 
         verify(pms).scheduleWritePackageRestrictions(eq(TEST_USER_ID))
-        verify(broadcastHelper).sendPackageBroadcast(eq(Intent.ACTION_DISTRACTING_PACKAGES_CHANGED),
-                nullable(), bundleCaptor.capture(), anyInt(), nullable(), nullable(), any(),
-                nullable(), nullable(), nullable(), nullable())
-        val modifiedPackages = bundleCaptor.value.getStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST)
-        val distractionFlags = bundleCaptor.value.getInt(Intent.EXTRA_DISTRACTION_RESTRICTIONS)
+        verify(broadcastHelper).sendDistractingPackagesChanged(
+                any(Computer::class.java), pkgListCaptor.capture(), any(), eq(TEST_USER_ID),
+                flagsCaptor.capture())
+        val modifiedPackages = pkgListCaptor.value
+        val distractionFlags = flagsCaptor.value
         assertThat(modifiedPackages).asList().containsExactly(TEST_PACKAGE_1, TEST_PACKAGE_2)
         assertThat(distractionFlags).isEqualTo(PackageManager.RESTRICTION_NONE)
     }
@@ -170,9 +167,8 @@
         testHandler.flush()
 
         verify(pms, never()).scheduleWritePackageRestrictions(eq(TEST_USER_ID))
-        verify(broadcastHelper, never()).sendPackageBroadcast(eq(
-                Intent.ACTION_DISTRACTING_PACKAGES_CHANGED), nullable(), nullable(), anyInt(),
-                nullable(), nullable(), any(), nullable(), nullable(), nullable(), nullable())
+        verify(broadcastHelper, never()).sendDistractingPackagesChanged(
+                any(), any(), any(), any(), any())
     }
 
     @Test
@@ -189,22 +185,21 @@
                 arrayOfNulls(0), TEST_USER_ID)
         testHandler.flush()
         verify(pms, never()).scheduleWritePackageRestrictions(eq(TEST_USER_ID))
-        verify(broadcastHelper, never()).sendPackageBroadcast(eq(
-                Intent.ACTION_DISTRACTING_PACKAGES_CHANGED), nullable(), nullable(), anyInt(),
-                nullable(), nullable(), any(), nullable(), nullable(), nullable(), nullable())
+        verify(broadcastHelper, never()).sendDistractingPackagesChanged(
+                any(), any(), any(), any(), any())
     }
 
     @Test
     fun sendDistractingPackagesChanged() {
-        distractingPackageHelper.sendDistractingPackagesChanged(packagesToChange, uidsToChange,
-                TEST_USER_ID, PackageManager.RESTRICTION_HIDE_NOTIFICATIONS)
+        broadcastHelper.sendDistractingPackagesChanged(pms.snapshotComputer(),
+                packagesToChange, uidsToChange, TEST_USER_ID,
+                PackageManager.RESTRICTION_HIDE_NOTIFICATIONS)
         testHandler.flush()
-        verify(broadcastHelper).sendPackageBroadcast(eq(Intent.ACTION_DISTRACTING_PACKAGES_CHANGED),
-                nullable(), bundleCaptor.capture(), anyInt(), nullable(), nullable(), any(),
-                nullable(), nullable(), nullable(), nullable())
+        verify(broadcastHelper).sendDistractingPackagesChanged(any(Computer::class.java),
+                pkgListCaptor.capture(), uidsCaptor.capture(), eq(TEST_USER_ID), any())
 
-        var changedPackages = bundleCaptor.value.getStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST)
-        var changedUids = bundleCaptor.value.getIntArray(Intent.EXTRA_CHANGED_UID_LIST)
+        var changedPackages = pkgListCaptor.value
+        var changedUids = uidsCaptor.value
         assertThat(changedPackages).asList().containsExactly(TEST_PACKAGE_1, TEST_PACKAGE_2)
         assertThat(changedUids).asList().containsExactly(
                 packageSetting1.appId, packageSetting2.appId)
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageHelperTestBase.kt b/services/tests/mockingservicestests/src/com/android/server/pm/PackageHelperTestBase.kt
index 5fd270e..eb00164 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageHelperTestBase.kt
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageHelperTestBase.kt
@@ -17,7 +17,6 @@
 package com.android.server.pm
 
 import android.os.Build
-import android.os.Bundle
 import android.os.UserHandle
 import android.os.UserManager
 import com.android.server.pm.pkg.PackageStateInternal
@@ -68,7 +67,11 @@
     lateinit var protectedPackages: ProtectedPackages
 
     @Captor
-    lateinit var bundleCaptor: ArgumentCaptor<Bundle>
+    lateinit var pkgListCaptor: ArgumentCaptor<Array<String>>
+    @Captor
+    lateinit var flagsCaptor: ArgumentCaptor<Int>
+    @Captor
+    lateinit var uidsCaptor: ArgumentCaptor<IntArray>
 
     @Rule
     @JvmField
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java
index c5db5db..6c44fd0 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java
@@ -17,12 +17,12 @@
 package com.android.server.pm;
 
 import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.after;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
 
 import android.content.Intent;
 import android.content.pm.PackageInstaller;
@@ -63,9 +63,7 @@
 
     @Before
     public void setup() {
-        when(mMockSystem.mocks().getInjector().getHandler()).thenReturn(mHandler);
-        mPackageMonitorCallbackHelper = new PackageMonitorCallbackHelper(
-                mMockSystem.mocks().getInjector());
+        mPackageMonitorCallbackHelper = new PackageMonitorCallbackHelper();
     }
 
 
@@ -80,7 +78,7 @@
 
         mPackageMonitorCallbackHelper.notifyPackageMonitor(Intent.ACTION_PACKAGE_ADDED,
                 FAKE_PACKAGE_NAME, createFakeBundle(), new int[]{0} /* userIds */,
-                null /* instantUserIds */, null /* broadcastAllowList */);
+                null /* instantUserIds */, null /* broadcastAllowList */, mHandler);
 
         verify(callback, after(WAIT_CALLBACK_CALLED_IN_MS).never()).sendResult(any());
     }
@@ -93,7 +91,7 @@
                 Binder.getCallingUid());
         mPackageMonitorCallbackHelper.notifyPackageMonitor(Intent.ACTION_PACKAGE_ADDED,
                 FAKE_PACKAGE_NAME, createFakeBundle(), new int[]{0}, null /* instantUserIds */,
-                null /* broadcastAllowList */);
+                null /* broadcastAllowList */, mHandler);
 
         verify(callback, after(WAIT_CALLBACK_CALLED_IN_MS).times(1)).sendResult(any());
 
@@ -101,7 +99,7 @@
         mPackageMonitorCallbackHelper.unregisterPackageMonitorCallback(callback);
         mPackageMonitorCallbackHelper.notifyPackageMonitor(Intent.ACTION_PACKAGE_ADDED,
                 FAKE_PACKAGE_NAME, createFakeBundle(), new int[]{0} /* userIds */,
-                null /* instantUserIds */, null /* broadcastAllowList */);
+                null /* instantUserIds */, null /* broadcastAllowList */, mHandler);
 
         verify(callback, after(WAIT_CALLBACK_CALLED_IN_MS).never()).sendResult(any());
     }
@@ -114,7 +112,7 @@
                 Binder.getCallingUid());
         mPackageMonitorCallbackHelper.notifyPackageMonitor(Intent.ACTION_PACKAGE_ADDED,
                 FAKE_PACKAGE_NAME, createFakeBundle(), new int[]{0} /* userIds */,
-                null /* instantUserIds */, null /* broadcastAllowList */);
+                null /* instantUserIds */, null /* broadcastAllowList */, mHandler);
 
         ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
         verify(callback, after(WAIT_CALLBACK_CALLED_IN_MS).times(1)).sendResult(
@@ -138,7 +136,7 @@
         // Notify for user 10
         mPackageMonitorCallbackHelper.notifyPackageMonitor(Intent.ACTION_PACKAGE_ADDED,
                 FAKE_PACKAGE_NAME, createFakeBundle(), new int[]{10} /* userIds */,
-                null /* instantUserIds */, null /* broadcastAllowList */);
+                null /* instantUserIds */, null /* broadcastAllowList */, mHandler);
 
         verify(callback, after(WAIT_CALLBACK_CALLED_IN_MS).never()).sendResult(any());
     }
@@ -155,7 +153,7 @@
         mPackageMonitorCallbackHelper.notifyPackageChanged(FAKE_PACKAGE_NAME,
                 false /* dontKillApp */, components, FAKE_PACKAGE_UID, null /* reason */,
                 new int[]{0} /* userIds */, null /* instantUserIds */,
-                null /* broadcastAllowList */);
+                null /* broadcastAllowList */, mHandler);
 
         ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
         verify(callback, after(WAIT_CALLBACK_CALLED_IN_MS).times(1)).sendResult(
@@ -183,7 +181,8 @@
                 Binder.getCallingUid());
         mPackageMonitorCallbackHelper.notifyPackageAddedForNewUsers(FAKE_PACKAGE_NAME,
                 FAKE_PACKAGE_UID, new int[]{0} /* userIds */, new int[0], false /* isArchived */,
-                PackageInstaller.DATA_LOADER_TYPE_STREAMING, null /* broadcastAllowList */);
+                PackageInstaller.DATA_LOADER_TYPE_STREAMING, null /* broadcastAllowList */,
+                mHandler);
 
         ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
         verify(callback, after(WAIT_CALLBACK_CALLED_IN_MS).times(1)).sendResult(
@@ -207,7 +206,7 @@
                 Binder.getCallingUid());
         mPackageMonitorCallbackHelper.notifyResourcesChanged(true /* mediaStatus */,
                 true /* replacing */, new String[]{FAKE_PACKAGE_NAME},
-                new int[]{FAKE_PACKAGE_UID} /* uids */);
+                new int[]{FAKE_PACKAGE_UID} /* uids */, mHandler);
 
         ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
         verify(callback, after(WAIT_CALLBACK_CALLED_IN_MS).times(1)).sendResult(
@@ -240,7 +239,7 @@
         mPackageMonitorCallbackHelper.onUserRemoved(10);
         mPackageMonitorCallbackHelper.notifyPackageMonitor(Intent.ACTION_PACKAGE_ADDED,
                 FAKE_PACKAGE_NAME, createFakeBundle(), new int[]{10} /* userIds */,
-                null /* instantUserIds */, null /* broadcastAllowList */);
+                null /* instantUserIds */, null /* broadcastAllowList */, mHandler);
 
         verify(callback, after(WAIT_CALLBACK_CALLED_IN_MS).never()).sendResult(any());
     }
@@ -256,7 +255,7 @@
                 Binder.getCallingUid());
         mPackageMonitorCallbackHelper.notifyPackageMonitor(Intent.ACTION_PACKAGE_ADDED,
                 FAKE_PACKAGE_NAME, createFakeBundle(), new int[]{0} /* userIds */,
-                null /* instantUserIds */, broadcastAllowList);
+                null /* instantUserIds */, broadcastAllowList, mHandler);
 
         verify(callback, after(WAIT_CALLBACK_CALLED_IN_MS).times(1)).sendResult(any());
     }
@@ -272,7 +271,7 @@
                 Binder.getCallingUid());
         mPackageMonitorCallbackHelper.notifyPackageMonitor(Intent.ACTION_PACKAGE_ADDED,
                 FAKE_PACKAGE_NAME, createFakeBundle(), new int[]{0} /* userIds */,
-                null /* instantUserIds */, broadcastAllowList);
+                null /* instantUserIds */, broadcastAllowList, mHandler);
 
         verify(callback, after(WAIT_CALLBACK_CALLED_IN_MS).never()).sendResult(any());
     }
@@ -288,7 +287,7 @@
                 Process.SYSTEM_UID);
         mPackageMonitorCallbackHelper.notifyPackageMonitor(Intent.ACTION_PACKAGE_ADDED,
                 FAKE_PACKAGE_NAME, createFakeBundle(), new int[]{0} /* userIds */,
-                null /* instantUserIds */, broadcastAllowList);
+                null /* instantUserIds */, broadcastAllowList, mHandler);
 
         verify(callback, after(WAIT_CALLBACK_CALLED_IN_MS).times(1)).sendResult(any());
     }
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt
index 6797576..4240373 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt
@@ -22,12 +22,11 @@
 import android.os.PersistableBundle
 import com.android.server.testutils.any
 import com.android.server.testutils.eq
-import com.android.server.testutils.nullable
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
-import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 
@@ -44,17 +43,10 @@
         testHandler.flush()
 
         verify(pms).scheduleWritePackageRestrictions(eq(TEST_USER_ID))
-        verify(broadcastHelper).sendPackageBroadcast(eq(Intent.ACTION_PACKAGES_SUSPENDED),
-            nullable(), bundleCaptor.capture(), anyInt(), nullable(), nullable(), any(),
-            nullable(), nullable(), nullable(), nullable())
-        verify(broadcastHelper).doSendBroadcast(eq(Intent.ACTION_MY_PACKAGE_SUSPENDED), nullable(),
-            nullable(), any(), eq(TEST_PACKAGE_1), nullable(), any(), any(), nullable(),
-            nullable(), nullable())
-        verify(broadcastHelper).doSendBroadcast(eq(Intent.ACTION_MY_PACKAGE_SUSPENDED), nullable(),
-            nullable(), any(), eq(TEST_PACKAGE_2), nullable(), any(), any(), nullable(),
-            nullable(), nullable())
+        verify(broadcastHelper).sendPackagesSuspendedOrUnsuspendedForUser(any(Computer::class.java),
+            eq(Intent.ACTION_PACKAGES_SUSPENDED), pkgListCaptor.capture(), any(), any(), any())
 
-        var modifiedPackages = bundleCaptor.value.getStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST)
+        var modifiedPackages = pkgListCaptor.value
         assertThat(modifiedPackages).asList().containsExactly(TEST_PACKAGE_1, TEST_PACKAGE_2)
         assertThat(failedNames).isEmpty()
     }
@@ -146,6 +138,7 @@
                 null /* launcherExtras */, null /* dialogInfo */, DEVICE_OWNER_PACKAGE,
                 TEST_USER_ID, deviceOwnerUid, false /* forQuietMode */, false /* quarantined */)
         testHandler.flush()
+        Mockito.clearInvocations(broadcastHelper)
         assertThat(failedNames).isEmpty()
         failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(),
                 targetPackages, false /* suspended */, null /* appExtras */,
@@ -154,17 +147,13 @@
         testHandler.flush()
 
         verify(pms, times(2)).scheduleWritePackageRestrictions(eq(TEST_USER_ID))
-        verify(broadcastHelper).sendPackageBroadcast(eq(Intent.ACTION_PACKAGES_UNSUSPENDED),
-            nullable(), bundleCaptor.capture(), anyInt(), nullable(), nullable(), any(),
-            nullable(), nullable(), nullable(), nullable())
-        verify(broadcastHelper).doSendBroadcast(eq(Intent.ACTION_MY_PACKAGE_UNSUSPENDED),
-            nullable(), nullable(), any(), eq(TEST_PACKAGE_1), nullable(), any(), any(),
-            nullable(), nullable(), nullable())
-        verify(broadcastHelper).doSendBroadcast(eq(Intent.ACTION_MY_PACKAGE_UNSUSPENDED),
-            nullable(), nullable(), any(), eq(TEST_PACKAGE_2), nullable(), any(), any(),
-            nullable(), nullable(), nullable())
+        verify(broadcastHelper).sendPackagesSuspendedOrUnsuspendedForUser(any(Computer::class.java),
+                eq(Intent.ACTION_PACKAGES_UNSUSPENDED), pkgListCaptor.capture(), any(), any(),
+                any())
+        verify(broadcastHelper).sendMyPackageSuspendedOrUnsuspended(any(Computer::class.java),
+                any(), any(), any())
 
-        var modifiedPackages = bundleCaptor.value.getStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST)
+        var modifiedPackages = pkgListCaptor.value
         assertThat(modifiedPackages).asList().containsExactly(TEST_PACKAGE_1, TEST_PACKAGE_2)
         assertThat(failedNames).isEmpty()
     }
@@ -206,7 +195,7 @@
         testHandler.flush()
         assertThat(failedNames).isEmpty()
 
-        val result = suspendPackageHelper.getSuspendedPackageAppExtras(pms.snapshotComputer(),
+        val result = SuspendPackageHelper.getSuspendedPackageAppExtras(pms.snapshotComputer(),
             TEST_PACKAGE_1, TEST_USER_ID, deviceOwnerUid)!!
 
         assertThat(result.getString(TEST_PACKAGE_1)).isEqualTo(TEST_PACKAGE_1)
@@ -222,14 +211,15 @@
                 null /* dialogInfo */, DEVICE_OWNER_PACKAGE, TEST_USER_ID, deviceOwnerUid,
                 false /* forQuietMode */, false /* quarantined */)
         testHandler.flush()
+        Mockito.clearInvocations(broadcastHelper)
         assertThat(failedNames).isEmpty()
         assertThat(suspendPackageHelper.getSuspendingPackage(pms.snapshotComputer(),
             TEST_PACKAGE_1, TEST_USER_ID, deviceOwnerUid)).isEqualTo(DEVICE_OWNER_PACKAGE)
         assertThat(suspendPackageHelper.getSuspendingPackage(pms.snapshotComputer(),
             TEST_PACKAGE_2, TEST_USER_ID, deviceOwnerUid)).isEqualTo(DEVICE_OWNER_PACKAGE)
-        assertThat(suspendPackageHelper.getSuspendedPackageAppExtras(pms.snapshotComputer(),
+        assertThat(SuspendPackageHelper.getSuspendedPackageAppExtras(pms.snapshotComputer(),
             TEST_PACKAGE_1, TEST_USER_ID, deviceOwnerUid)).isNotNull()
-        assertThat(suspendPackageHelper.getSuspendedPackageAppExtras(pms.snapshotComputer(),
+        assertThat(SuspendPackageHelper.getSuspendedPackageAppExtras(pms.snapshotComputer(),
             TEST_PACKAGE_2, TEST_USER_ID, deviceOwnerUid)).isNotNull()
 
         suspendPackageHelper.removeSuspensionsBySuspendingPackage(pms.snapshotComputer(),
@@ -238,23 +228,18 @@
 
         testHandler.flush()
         verify(pms, times(2)).scheduleWritePackageRestrictions(eq(TEST_USER_ID))
-        verify(broadcastHelper).sendPackageBroadcast(eq(Intent.ACTION_PACKAGES_UNSUSPENDED),
-            nullable(), bundleCaptor.capture(), anyInt(), nullable(), nullable(), any(),
-            nullable(), nullable(), nullable(), nullable())
-        verify(broadcastHelper).doSendBroadcast(eq(Intent.ACTION_MY_PACKAGE_UNSUSPENDED),
-            nullable(), nullable(), any(), eq(TEST_PACKAGE_1), nullable(), any(), any(),
-            nullable(), nullable(), nullable())
-        verify(broadcastHelper).doSendBroadcast(eq(Intent.ACTION_MY_PACKAGE_UNSUSPENDED),
-            nullable(), nullable(), any(), eq(TEST_PACKAGE_2), nullable(), any(), any(),
-            nullable(), nullable(), nullable())
+        verify(broadcastHelper).sendPackagesSuspendedOrUnsuspendedForUser(any(Computer::class.java),
+                eq(Intent.ACTION_PACKAGES_UNSUSPENDED), any(), any(), any(), any())
+        verify(broadcastHelper).sendMyPackageSuspendedOrUnsuspended(any(Computer::class.java),
+                any(), any(), any())
 
         assertThat(suspendPackageHelper.getSuspendingPackage(pms.snapshotComputer(),
             TEST_PACKAGE_1, TEST_USER_ID, deviceOwnerUid)).isNull()
         assertThat(suspendPackageHelper.getSuspendingPackage(pms.snapshotComputer(),
             TEST_PACKAGE_2, TEST_USER_ID, deviceOwnerUid)).isNull()
-        assertThat(suspendPackageHelper.getSuspendedPackageAppExtras(pms.snapshotComputer(),
+        assertThat(SuspendPackageHelper.getSuspendedPackageAppExtras(pms.snapshotComputer(),
             TEST_PACKAGE_1, TEST_USER_ID, deviceOwnerUid)).isNull()
-        assertThat(suspendPackageHelper.getSuspendedPackageAppExtras(pms.snapshotComputer(),
+        assertThat(SuspendPackageHelper.getSuspendedPackageAppExtras(pms.snapshotComputer(),
             TEST_PACKAGE_2, TEST_USER_ID, deviceOwnerUid)).isNull()
     }
 
@@ -319,39 +304,4 @@
 
         assertThat(result.title).isEqualTo(TEST_PACKAGE_1)
     }
-
-    @Test
-    @Throws(Exception::class)
-    fun sendPackagesSuspendedForUser() {
-        suspendPackageHelper.sendPackagesSuspendedForUser(
-            Intent.ACTION_PACKAGES_SUSPENDED, packagesToChange, uidsToChange, false, TEST_USER_ID)
-        testHandler.flush()
-        verify(broadcastHelper).sendPackageBroadcast(any(), nullable(), bundleCaptor.capture(),
-                anyInt(), nullable(), nullable(), any(), nullable(), nullable(), nullable(),
-                nullable())
-
-        var changedPackages = bundleCaptor.value.getStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST)
-        var changedUids = bundleCaptor.value.getIntArray(Intent.EXTRA_CHANGED_UID_LIST)
-        assertThat(changedPackages).asList().containsExactly(TEST_PACKAGE_1, TEST_PACKAGE_2)
-        assertThat(changedUids).asList().containsExactly(
-                packageSetting1.appId, packageSetting2.appId)
-    }
-
-    @Test
-    @Throws(Exception::class)
-    fun sendPackagesSuspendModifiedForUser() {
-        suspendPackageHelper.sendPackagesSuspendedForUser(
-            Intent.ACTION_PACKAGES_SUSPENSION_CHANGED, packagesToChange, uidsToChange, false, TEST_USER_ID)
-        testHandler.flush()
-        verify(broadcastHelper).sendPackageBroadcast(
-                eq(Intent.ACTION_PACKAGES_SUSPENSION_CHANGED), nullable(), bundleCaptor.capture(),
-                anyInt(), nullable(), nullable(), any(), nullable(), nullable(), nullable(),
-                nullable())
-
-        var modifiedPackages = bundleCaptor.value.getStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST)
-        var modifiedUids = bundleCaptor.value.getIntArray(Intent.EXTRA_CHANGED_UID_LIST)
-        assertThat(modifiedPackages).asList().containsExactly(TEST_PACKAGE_1, TEST_PACKAGE_2)
-        assertThat(modifiedUids).asList().containsExactly(
-                packageSetting1.appId, packageSetting2.appId)
-    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java
index a0bca3b..24ad976 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java
@@ -50,7 +50,6 @@
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.UserHandle;
-import android.platform.test.annotations.FlakyTest;
 import android.provider.Settings;
 import android.test.mock.MockContentResolver;
 import android.view.InputDevice;
@@ -60,6 +59,7 @@
 import android.view.accessibility.MagnificationAnimationCallback;
 
 import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.FlakyTest;
 
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.server.LocalServices;
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index 41af9e3..1e6306c 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -57,6 +57,7 @@
 import android.companion.virtual.VirtualDeviceParams;
 import android.companion.virtual.audio.IAudioConfigChangedCallback;
 import android.companion.virtual.audio.IAudioRoutingCallback;
+import android.companion.virtual.flags.Flags;
 import android.companion.virtual.sensor.IVirtualSensorCallback;
 import android.companion.virtual.sensor.VirtualSensor;
 import android.companion.virtual.sensor.VirtualSensorCallback;
@@ -100,6 +101,7 @@
 import android.os.RemoteException;
 import android.os.WorkSource;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.util.ArraySet;
@@ -211,6 +213,9 @@
     private static final String TEST_SITE = "http://test";
 
     @Rule
+    public SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    @Rule
     public AdoptShellPermissionsRule mAdoptShellPermissionsRule = new AdoptShellPermissionsRule(
             InstrumentationRegistry.getInstrumentation().getUiAutomation(),
             Manifest.permission.CREATE_VIRTUAL_DEVICE);
@@ -328,6 +333,11 @@
         LocalServices.removeServiceForTest(DisplayManagerInternal.class);
         LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock);
 
+        mSetFlagsRule.disableFlags(Flags.FLAG_VDM_PUBLIC_APIS);
+        mSetFlagsRule.disableFlags(Flags.FLAG_DYNAMIC_POLICY);
+        mSetFlagsRule.disableFlags(Flags.FLAG_STREAM_PERMISSIONS);
+        mSetFlagsRule.disableFlags(Flags.FLAG_VDM_CUSTOM_HOME);
+
         doReturn(true).when(mInputManagerInternalMock).setVirtualMousePointerDisplayId(anyInt());
         doNothing().when(mInputManagerInternalMock).setPointerAcceleration(anyFloat(), anyInt());
         doNothing().when(mInputManagerInternalMock).setPointerIconVisible(anyBoolean(), anyInt());
@@ -1450,6 +1460,50 @@
     }
 
     @Test
+    public void openPermissionControllerOnVirtualDisplay_displayOnRemoteDevices_startsWhenFlagIsEnabled() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_STREAM_PERMISSIONS);
+        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+        GenericWindowPolicyController gwpc = mDeviceImpl.getDisplayWindowPolicyControllerForTest(
+                DISPLAY_ID_1);
+        doNothing().when(mContext).startActivityAsUser(any(), any(), any());
+
+        ActivityInfo activityInfo = getActivityInfo(
+                PERMISSION_CONTROLLER_PACKAGE_NAME,
+                PERMISSION_CONTROLLER_PACKAGE_NAME,
+                /* displayOnRemoveDevices */ true,
+                /* targetDisplayCategory */ null);
+        Intent blockedAppIntent = BlockedAppStreamingActivity.createIntent(
+                activityInfo, mAssociationInfo.getDisplayName());
+        gwpc.canActivityBeLaunched(activityInfo, blockedAppIntent,
+                WindowConfiguration.WINDOWING_MODE_FULLSCREEN, DISPLAY_ID_1, /*isNewTask=*/false);
+
+        verify(mContext, never()).startActivityAsUser(argThat(intent ->
+                intent.filterEquals(blockedAppIntent)), any(), any());
+    }
+
+    @Test
+    public void openPermissionControllerOnVirtualDisplay_dontDisplayOnRemoteDevices_startsWhenFlagIsEnabled() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_STREAM_PERMISSIONS);
+        addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+        GenericWindowPolicyController gwpc = mDeviceImpl.getDisplayWindowPolicyControllerForTest(
+                DISPLAY_ID_1);
+        doNothing().when(mContext).startActivityAsUser(any(), any(), any());
+
+        ActivityInfo activityInfo = getActivityInfo(
+                PERMISSION_CONTROLLER_PACKAGE_NAME,
+                PERMISSION_CONTROLLER_PACKAGE_NAME,
+                /* displayOnRemoveDevices */ false,
+                /* targetDisplayCategory */ null);
+        Intent blockedAppIntent = BlockedAppStreamingActivity.createIntent(
+                activityInfo, mAssociationInfo.getDisplayName());
+        gwpc.canActivityBeLaunched(activityInfo, blockedAppIntent,
+                WindowConfiguration.WINDOWING_MODE_FULLSCREEN, DISPLAY_ID_1, /*isNewTask=*/false);
+
+        verify(mContext).startActivityAsUser(argThat(intent ->
+                intent.filterEquals(blockedAppIntent)), any(), any());
+    }
+
+    @Test
     public void openSettingsOnVirtualDisplay_startBlockedAlertActivity() {
         addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
         GenericWindowPolicyController gwpc = mDeviceImpl.getDisplayWindowPolicyControllerForTest(
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java
index c65452a..90d9452 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java
@@ -49,6 +49,7 @@
     private static final int VIRTUAL_DEVICE_ID = 42;
     private static final String PERSISTENT_ID = "persistentId";
     private static final String DEVICE_NAME = "VirtualDeviceName";
+    private static final String DISPLAY_NAME = "DisplayName";
 
     @Mock
     private IVirtualDevice mVirtualDevice;
@@ -87,7 +88,8 @@
     @Test
     public void parcelable_shouldRecreateSuccessfully() {
         VirtualDevice originalDevice =
-                new VirtualDevice(mVirtualDevice, VIRTUAL_DEVICE_ID, PERSISTENT_ID, DEVICE_NAME);
+                new VirtualDevice(mVirtualDevice, VIRTUAL_DEVICE_ID, PERSISTENT_ID, DEVICE_NAME,
+                        DISPLAY_NAME);
         Parcel parcel = Parcel.obtain();
         originalDevice.writeToParcel(parcel, 0);
         parcel.setDataPosition(0);
@@ -96,6 +98,7 @@
         assertThat(device.getDeviceId()).isEqualTo(VIRTUAL_DEVICE_ID);
         assertThat(device.getPersistentDeviceId()).isEqualTo(PERSISTENT_ID);
         assertThat(device.getName()).isEqualTo(DEVICE_NAME);
+        assertThat(device.getDisplayName().toString()).isEqualTo(DISPLAY_NAME);
     }
 
     @RequiresFlagsEnabled(Flags.FLAG_VDM_PUBLIC_APIS)
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
index 1c48b8a..ef5270e 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
@@ -82,6 +82,7 @@
                         /* activityPolicyExemptions= */ new ArraySet<>(),
                         /* crossTaskNavigationAllowedByDefault= */ true,
                         /* crossTaskNavigationExemptions= */ new ArraySet<>(),
+                        /* permissionDialogComponent */ null,
                         /* activityListener= */ null,
                         /* pipBlockedCallback= */ null,
                         /* activityBlockedCallback= */ null,
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
index dee7780..37a6d22 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
@@ -781,7 +781,7 @@
                 password, PRIMARY_USER_ID, 0 /* flags */).getResponseCode());
         assertTrue(mLocalService.isEscrowTokenActive(handle, PRIMARY_USER_ID));
 
-        mService.onCleanupUser(PRIMARY_USER_ID);
+        mService.onUserStopped(PRIMARY_USER_ID);
         assertNull(mLocalService.getUserPasswordMetrics(PRIMARY_USER_ID));
 
         assertTrue(mLocalService.unlockUserWithToken(handle, token, PRIMARY_USER_ID));
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/WeakEscrowTokenTests.java b/services/tests/servicestests/src/com/android/server/locksettings/WeakEscrowTokenTests.java
index 2c9ba34..e8b7ad7 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/WeakEscrowTokenTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/WeakEscrowTokenTests.java
@@ -169,7 +169,7 @@
         assertTrue(mService.isWeakEscrowTokenActive(handle, PRIMARY_USER_ID));
         assertTrue(mService.isWeakEscrowTokenValid(handle, token, PRIMARY_USER_ID));
 
-        mService.onCleanupUser(PRIMARY_USER_ID);
+        mService.onUserStopped(PRIMARY_USER_ID);
         assertNull(mLocalService.getUserPasswordMetrics(PRIMARY_USER_ID));
 
         assertTrue(mLocalService.unlockUserWithToken(handle, token, PRIMARY_USER_ID));
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index f65cb93..40ac7b1c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -2855,14 +2855,14 @@
                 .setTask(sourceRecord.getTask()).build();
         secondRecord.showStartingWindow(null /* prev */, true /* newTask */, false,
                 true /* startActivity */, sourceRecord);
-        assertTrue(secondRecord.mAllowIconSplashScreen);
+        assertFalse(secondRecord.mSplashScreenStyleSolidColor);
         secondRecord.onStartingWindowDrawn();
 
         final ActivityRecord finalRecord = new ActivityBuilder(mAtm)
                 .setTask(sourceRecord.getTask()).build();
         finalRecord.showStartingWindow(null /* prev */, true /* newTask */, false,
                 true /* startActivity */, secondRecord);
-        assertFalse(finalRecord.mAllowIconSplashScreen);
+        assertTrue(finalRecord.mSplashScreenStyleSolidColor);
     }
 
     @Test
diff --git a/tools/lint/fix/soong_lint_fix.py b/tools/lint/fix/soong_lint_fix.py
index 4a2e37e..2e82beb 100644
--- a/tools/lint/fix/soong_lint_fix.py
+++ b/tools/lint/fix/soong_lint_fix.py
@@ -14,6 +14,7 @@
 
 import argparse
 import json
+import functools
 import os
 import shutil
 import subprocess
@@ -28,6 +29,7 @@
 PATH_PREFIX = "out/soong/.intermediates"
 PATH_SUFFIX = "android_common/lint"
 FIX_ZIP = "suggested-fixes.zip"
+MODULE_JAVA_DEPS = "out/soong/module_bp_java_deps.json"
 
 
 class SoongModule:
@@ -49,11 +51,26 @@
         print(f"Found module {partial_path}/{self._name}.")
         self._path = f"{PATH_PREFIX}/{partial_path}/{self._name}/{PATH_SUFFIX}"
 
+    def find_java_deps(self, module_java_deps):
+        """Finds the dependencies of a Java module in the loaded module_bp_java_deps.json.
+
+        Returns:
+            A list of module names.
+        """
+        if self._name not in module_java_deps:
+            raise Exception(f"Module {self._name} not found!")
+
+        return module_java_deps[self._name]["dependencies"]
+
     @property
     def name(self):
         return self._name
 
     @property
+    def path(self):
+        return self._path
+
+    @property
     def lint_report(self):
         return f"{self._path}/lint-report.txt"
 
@@ -62,52 +79,25 @@
         return f"{self._path}/{FIX_ZIP}"
 
 
-class SoongLintFix:
+class SoongLintWrapper:
     """
-    This class creates a command line tool that will apply lint fixes to the
-    platform via the necessary combination of soong and shell commands.
+    This class wraps the necessary calls to Soong and/or shell commands to lint
+    platform modules and apply suggested fixes if desired.
 
-    It breaks up these operations into a few "private" methods that are
-    intentionally exposed so experimental code can tweak behavior.
-
-    The entry point, `run`, will apply lint fixes using the intermediate
-    `suggested-fixes` directory that soong creates during its invocation of
-    lint.
-
-    Basic usage:
-    ```
-    from soong_lint_fix import SoongLintFix
-
-    opts = SoongLintFixOptions()
-    opts.parse_args(sys.argv)
-    SoongLintFix(opts).run()
-    ```
+    It breaks up these operations into a few methods that are available to
+    sub-classes (see SoongLintFix for an example).
     """
-    def __init__(self, opts):
-        self._opts = opts
+    def __init__(self, check=None, lint_module=None):
+        self._check = check
+        self._lint_module = lint_module
         self._kwargs = None
-        self._modules = []
-
-    def run(self):
-        """
-        Run the script
-        """
-        self._setup()
-        self._find_modules()
-        self._lint()
-
-        if not self._opts.no_fix:
-            self._fix()
-
-        if self._opts.print:
-            self._print()
 
     def _setup(self):
         env = os.environ.copy()
-        if self._opts.check:
-            env["ANDROID_LINT_CHECK"] = self._opts.check
-        if self._opts.lint_module:
-            env["ANDROID_LINT_CHECK_EXTRA_MODULES"] = self._opts.lint_module
+        if self._check:
+            env["ANDROID_LINT_CHECK"] = self._check
+        if self._lint_module:
+            env["ANDROID_LINT_CHECK_EXTRA_MODULES"] = self._lint_module
 
         self._kwargs = {
             "env": env,
@@ -117,7 +107,10 @@
 
         os.chdir(ANDROID_BUILD_TOP)
 
-        print("Refreshing soong modules...")
+    @functools.cached_property
+    def _module_info(self):
+        """Returns the JSON content of module-info.json."""
+        print("Refreshing Soong modules...")
         try:
             os.mkdir(ANDROID_PRODUCT_OUT)
         except OSError:
@@ -125,19 +118,54 @@
         subprocess.call(f"{SOONG_UI} --make-mode {PRODUCT_OUT}/module-info.json", **self._kwargs)
         print("done.")
 
-
-    def _find_modules(self):
         with open(f"{ANDROID_PRODUCT_OUT}/module-info.json") as f:
-            module_info = json.load(f)
+            return json.load(f)
 
-        for module_name in self._opts.modules:
-            module = SoongModule(module_name)
-            module.find(module_info)
-            self._modules.append(module)
+    def _find_module(self, module_name):
+        """Returns a SoongModule from a module name.
 
-    def _lint(self):
+        Ensures that the module is known to Soong.
+        """
+        module = SoongModule(module_name)
+        module.find(self._module_info)
+        return module
+
+    def _find_modules(self, module_names):
+        modules = []
+        for module_name in module_names:
+            modules.append(self._find_module(module_name))
+        return modules
+
+    @functools.cached_property
+    def _module_java_deps(self):
+        """Returns the JSON content of module_bp_java_deps.json."""
+        print("Refreshing Soong Java deps...")
+        subprocess.call(f"{SOONG_UI} --make-mode {MODULE_JAVA_DEPS}", **self._kwargs)
+        print("done.")
+
+        with open(f"{MODULE_JAVA_DEPS}") as f:
+            return json.load(f)
+
+    def _find_module_java_deps(self, module):
+        """Returns a list a dependencies for a module.
+
+        Args:
+            module: A SoongModule.
+
+        Returns:
+            A list of SoongModule.
+        """
+        deps = []
+        dep_names = module.find_java_deps(self._module_java_deps)
+        for dep_name in dep_names:
+            dep = SoongModule(dep_name)
+            dep.find(self._module_info)
+            deps.append(dep)
+        return deps
+
+    def _lint(self, modules):
         print("Cleaning up any old lint results...")
-        for module in self._modules:
+        for module in modules:
             try:
                 os.remove(f"{module.lint_report}")
                 os.remove(f"{module.suggested_fixes}")
@@ -145,13 +173,13 @@
                 pass
         print("done.")
 
-        target = " ".join([ module.lint_report for module in self._modules ])
+        target = " ".join([ module.lint_report for module in modules ])
         print(f"Generating {target}")
         subprocess.call(f"{SOONG_UI} --make-mode {target}", **self._kwargs)
         print("done.")
 
-    def _fix(self):
-        for module in self._modules:
+    def _fix(self, modules):
+        for module in modules:
             print(f"Copying suggested fixes for {module.name} to the tree...")
             with zipfile.ZipFile(f"{module.suggested_fixes}") as zip:
                 for name in zip.namelist():
@@ -161,13 +189,40 @@
                         shutil.copyfileobj(src, dst)
             print("done.")
 
-    def _print(self):
-        for module in self._modules:
+    def _print(self, modules):
+        for module in modules:
             print(f"### lint-report.txt {module.name} ###", end="\n\n")
             with open(module.lint_report, "r") as f:
                 print(f.read())
 
 
+class SoongLintFix(SoongLintWrapper):
+    """
+    Basic usage:
+    ```
+    from soong_lint_fix import SoongLintFix
+
+    opts = SoongLintFixOptions()
+    opts.parse_args()
+    SoongLintFix(opts).run()
+    ```
+    """
+    def __init__(self, opts):
+        super().__init__(check=opts.check, lint_module=opts.lint_module)
+        self._opts = opts
+
+    def run(self):
+        self._setup()
+        modules = self._find_modules(self._opts.modules)
+        self._lint(modules)
+
+        if not self._opts.no_fix:
+            self._fix(modules)
+
+        if self._opts.print:
+            self._print(modules)
+
+
 class SoongLintFixOptions:
     """Options for SoongLintFix"""
 
diff --git a/tools/lint/utils/enforce_permission_counter.py b/tools/lint/utils/enforce_permission_counter.py
index b5c2ffe..a4c00f7 100644
--- a/tools/lint/utils/enforce_permission_counter.py
+++ b/tools/lint/utils/enforce_permission_counter.py
@@ -16,57 +16,38 @@
 
 import soong_lint_fix
 
-# Libraries that constitute system_server.
-# It is non-trivial to keep in sync with services/Android.bp as some
-# module are post-processed (e.g, services.core).
-TARGETS = [
-        "services.core.unboosted",
-        "services.accessibility",
-        "services.appprediction",
-        "services.appwidget",
-        "services.autofill",
-        "services.backup",
-        "services.companion",
-        "services.contentcapture",
-        "services.contentsuggestions",
-        "services.coverage",
-        "services.devicepolicy",
-        "services.midi",
-        "services.musicsearch",
-        "services.net",
-        "services.people",
-        "services.print",
-        "services.profcollect",
-        "services.restrictions",
-        "services.searchui",
-        "services.smartspace",
-        "services.systemcaptions",
-        "services.translation",
-        "services.texttospeech",
-        "services.usage",
-        "services.usb",
-        "services.voiceinteraction",
-        "services.wallpapereffectsgeneration",
-        "services.wifi",
-]
+CHECK = "AnnotatedAidlCounter"
+LINT_MODULE = "AndroidUtilsLintChecker"
 
-
-class EnforcePermissionMigratedCounter:
+class EnforcePermissionMigratedCounter(soong_lint_fix.SoongLintWrapper):
     """Wrapper around lint_fix to count the number of AIDL methods annotated."""
+
+    def __init__(self):
+        super().__init__(check=CHECK, lint_module=LINT_MODULE)
+
     def run(self):
-        opts = soong_lint_fix.SoongLintFixOptions()
-        opts.check = "AnnotatedAidlCounter"
-        opts.lint_module = "AndroidUtilsLintChecker"
-        opts.no_fix = True
-        opts.modules = TARGETS
+        self._setup()
 
-        self.linter = soong_lint_fix.SoongLintFix(opts)
-        self.linter.run()
-        self.parse_lint_reports()
+        # Analyze the dependencies of the "services" module and the module
+        # "services.core.unboosted".
+        service_module = self._find_module("services")
+        dep_modules = self._find_module_java_deps(service_module) + \
+                      [self._find_module("services.core.unboosted")]
 
-    def parse_lint_reports(self):
+        # Skip dependencies that are not services. Skip the "services.core"
+        # module which is analyzed via "services.core.unboosted".
+        modules = []
+        for module in dep_modules:
+            if "frameworks/base/services" not in module.path:
+                continue
+            if module.name == "services.core":
+                continue
+            modules.append(module)
+
+        self._lint(modules)
+
         counts = { "unannotated": 0, "enforced": 0, "notRequired": 0 }
-        for module in self.linter._modules:
+        for module in modules:
             with open(module.lint_report, "r") as f:
                 content = f.read()
                 keys = dict(re.findall(r'(\w+)=(\d+)', content))