Merge "Fix Overlapping notifications when the swiping out is canceled" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index a271d06..08a09e1 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -15,6 +15,7 @@
 aconfig_srcjars = [
     ":android.app.usage.flags-aconfig-java{.generated_srcjars}",
     ":android.content.pm.flags-aconfig-java{.generated_srcjars}",
+    ":android.hardware.radio.flags-aconfig-java{.generated_srcjars}",
     ":android.nfc.flags-aconfig-java{.generated_srcjars}",
     ":android.os.flags-aconfig-java{.generated_srcjars}",
     ":android.os.vibrator.flags-aconfig-java{.generated_srcjars}",
@@ -36,6 +37,7 @@
     ":hwui_flags_java_lib{.generated_srcjars}",
     ":display_flags_lib{.generated_srcjars}",
     ":android.multiuser.flags-aconfig-java{.generated_srcjars}",
+    ":android.app.flags-aconfig-java{.generated_srcjars}",
 ]
 
 filegroup {
@@ -327,3 +329,29 @@
     aconfig_declarations: "android.multiuser.flags-aconfig",
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
+
+// Activity Manager
+aconfig_declarations {
+    name: "android.app.flags-aconfig",
+    package: "android.app",
+    srcs: ["core/java/android/app/activity_manager.aconfig"],
+}
+
+java_aconfig_library {
+    name: "android.app.flags-aconfig-java",
+    aconfig_declarations: "android.app.flags-aconfig",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
+// Broadcast Radio
+aconfig_declarations {
+    name: "android.hardware.radio.flags-aconfig",
+    package: "android.hardware.radio",
+    srcs: ["core/java/android/hardware/radio/*.aconfig"],
+}
+
+java_aconfig_library {
+    name: "android.hardware.radio.flags-aconfig-java",
+    aconfig_declarations: "android.hardware.radio.flags-aconfig",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp
index 8989b10..e5e0ad3 100644
--- a/api/StubLibraries.bp
+++ b/api/StubLibraries.bp
@@ -67,6 +67,7 @@
             tag: ".removed-api.txt",
         },
     ],
+    api_surface: "public",
 }
 
 priv_apps = " --show-annotation android.annotation.SystemApi\\(" +
@@ -85,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",
@@ -120,10 +124,14 @@
             tag: ".removed-api.txt",
         },
     ],
+    api_surface: "system",
 }
 
 droidstubs {
     name: "test-api-stubs-docs-non-updatable",
+    srcs: [
+        ":framework-minus-apex-aconfig-srcjars",
+    ],
     defaults: [
         "android-non-updatable-stubs-defaults",
         "module-classpath-stubs-defaults",
@@ -166,10 +174,14 @@
             tag: ".removed-api.txt",
         },
     ],
+    api_surface: "test",
 }
 
 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",
@@ -205,6 +217,7 @@
             tag: ".removed-api.txt",
         },
     ],
+    api_surface: "module-lib",
 }
 
 /////////////////////////////////////////////////////////////////////
@@ -381,8 +394,8 @@
 java_api_library {
     name: "android-non-updatable.stubs.from-text",
     api_surface: "public",
-    api_files: [
-        ":non-updatable-current.txt",
+    api_contributions: [
+        "api-stubs-docs-non-updatable.api.contribution",
     ],
     defaults: ["android-non-updatable_from_text_defaults"],
     full_api_surface_stub: "android_stubs_current.from-text",
@@ -391,9 +404,9 @@
 java_api_library {
     name: "android-non-updatable.stubs.system.from-text",
     api_surface: "system",
-    api_files: [
-        ":non-updatable-current.txt",
-        ":non-updatable-system-current.txt",
+    api_contributions: [
+        "api-stubs-docs-non-updatable.api.contribution",
+        "system-api-stubs-docs-non-updatable.api.contribution",
     ],
     defaults: ["android-non-updatable_from_text_defaults"],
     full_api_surface_stub: "android_system_stubs_current.from-text",
@@ -402,10 +415,10 @@
 java_api_library {
     name: "android-non-updatable.stubs.test.from-text",
     api_surface: "test",
-    api_files: [
-        ":non-updatable-current.txt",
-        ":non-updatable-system-current.txt",
-        ":non-updatable-test-current.txt",
+    api_contributions: [
+        "api-stubs-docs-non-updatable.api.contribution",
+        "system-api-stubs-docs-non-updatable.api.contribution",
+        "test-api-stubs-docs-non-updatable.api.contribution",
     ],
     defaults: ["android-non-updatable_from_text_defaults"],
     full_api_surface_stub: "android_test_stubs_current.from-text",
@@ -414,10 +427,10 @@
 java_api_library {
     name: "android-non-updatable.stubs.module_lib.from-text",
     api_surface: "module_lib",
-    api_files: [
-        ":non-updatable-current.txt",
-        ":non-updatable-system-current.txt",
-        ":non-updatable-module-lib-current.txt",
+    api_contributions: [
+        "api-stubs-docs-non-updatable.api.contribution",
+        "system-api-stubs-docs-non-updatable.api.contribution",
+        "module-lib-api-stubs-docs-non-updatable.api.contribution",
     ],
     defaults: ["android-non-updatable_from_text_defaults"],
     full_api_surface_stub: "android_module_lib_stubs_current_full.from-text",
@@ -615,7 +628,6 @@
     name: "android_test_stubs_current_contributions",
     api_surface: "test",
     api_contributions: [
-        "test-api-stubs-docs-non-updatable.api.contribution",
         "framework-virtualization.stubs.source.test.api.contribution",
         "framework-location.stubs.source.test.api.contribution",
     ],
@@ -690,6 +702,7 @@
     api_contributions: [
         "api-stubs-docs-non-updatable.api.contribution",
         "system-api-stubs-docs-non-updatable.api.contribution",
+        "test-api-stubs-docs-non-updatable.api.contribution",
     ],
     visibility: ["//visibility:public"],
 }
diff --git a/core/api/current.txt b/core/api/current.txt
index 1eb9e97..955858b 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -4616,9 +4616,9 @@
 
   public class ActivityManager {
     method public int addAppTask(@NonNull android.app.Activity, @NonNull android.content.Intent, @Nullable android.app.ActivityManager.TaskDescription, @NonNull android.graphics.Bitmap);
-    method public void addStartInfoTimestamp(@IntRange(from=android.app.ApplicationStartInfo.START_TIMESTAMP_RESERVED_RANGE_DEVELOPER_START, to=android.app.ApplicationStartInfo.START_TIMESTAMP_RESERVED_RANGE_DEVELOPER) int, long);
+    method @FlaggedApi("android.app.app_start_info") public void addStartInfoTimestamp(@IntRange(from=android.app.ApplicationStartInfo.START_TIMESTAMP_RESERVED_RANGE_DEVELOPER_START, to=android.app.ApplicationStartInfo.START_TIMESTAMP_RESERVED_RANGE_DEVELOPER) int, long);
     method public void appNotResponding(@NonNull String);
-    method public void clearApplicationStartInfoCompletionListener();
+    method @FlaggedApi("android.app.app_start_info") public void clearApplicationStartInfoCompletionListener();
     method public boolean clearApplicationUserData();
     method public void clearWatchHeapLimit();
     method @RequiresPermission(android.Manifest.permission.DUMP) public void dumpPackageState(java.io.FileDescriptor, String);
@@ -4626,7 +4626,7 @@
     method public java.util.List<android.app.ActivityManager.AppTask> getAppTasks();
     method public android.content.pm.ConfigurationInfo getDeviceConfigurationInfo();
     method @NonNull public java.util.List<android.app.ApplicationExitInfo> getHistoricalProcessExitReasons(@Nullable String, @IntRange(from=0) int, @IntRange(from=0) int);
-    method @NonNull public java.util.List<android.app.ApplicationStartInfo> getHistoricalProcessStartReasons(@IntRange(from=0) int);
+    method @FlaggedApi("android.app.app_start_info") @NonNull public java.util.List<android.app.ApplicationStartInfo> getHistoricalProcessStartReasons(@IntRange(from=0) int);
     method public int getLargeMemoryClass();
     method public int getLauncherLargeIconDensity();
     method public int getLauncherLargeIconSize();
@@ -4653,7 +4653,7 @@
     method @RequiresPermission(android.Manifest.permission.REORDER_TASKS) public void moveTaskToFront(int, int);
     method @RequiresPermission(android.Manifest.permission.REORDER_TASKS) public void moveTaskToFront(int, int, android.os.Bundle);
     method @Deprecated public void restartPackage(String);
-    method public void setApplicationStartInfoCompletionListener(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.app.ApplicationStartInfo>);
+    method @FlaggedApi("android.app.app_start_info") public void setApplicationStartInfoCompletionListener(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.app.ApplicationStartInfo>);
     method public void setProcessStateSummary(@Nullable byte[]);
     method public static void setVrThread(int);
     method public void setWatchHeapLimit(long);
@@ -5234,7 +5234,7 @@
     field public static final int REASON_USER_STOPPED = 11; // 0xb
   }
 
-  public final class ApplicationStartInfo implements android.os.Parcelable {
+  @FlaggedApi("android.app.app_start_info") public final class ApplicationStartInfo implements android.os.Parcelable {
     method public int describeContents();
     method public int getDefiningUid();
     method @Nullable public android.content.Intent getIntent();
@@ -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();
@@ -17563,7 +17564,7 @@
     ctor public FontFamily.Builder(@NonNull android.graphics.fonts.Font);
     method @NonNull public android.graphics.fonts.FontFamily.Builder addFont(@NonNull android.graphics.fonts.Font);
     method @NonNull public android.graphics.fonts.FontFamily build();
-    method @Nullable public android.graphics.fonts.FontFamily buildVariableFamily();
+    method @FlaggedApi("com.android.text.flags.deprecate_fonts_xml") @Nullable public android.graphics.fonts.FontFamily buildVariableFamily();
   }
 
   public final class FontStyle {
@@ -17656,7 +17657,7 @@
     method @FlaggedApi("com.android.text.flags.no_break_no_hyphenation_span") public int getHyphenation();
     method public int getLineBreakStyle();
     method public int getLineBreakWordStyle();
-    method @NonNull public android.graphics.text.LineBreakConfig merge(@NonNull android.graphics.text.LineBreakConfig);
+    method @FlaggedApi("com.android.text.flags.no_break_no_hyphenation_span") @NonNull public android.graphics.text.LineBreakConfig merge(@NonNull android.graphics.text.LineBreakConfig);
     field @FlaggedApi("com.android.text.flags.no_break_no_hyphenation_span") public static final int HYPHENATION_DISABLED = 0; // 0x0
     field @FlaggedApi("com.android.text.flags.no_break_no_hyphenation_span") public static final int HYPHENATION_ENABLED = 1; // 0x1
     field @FlaggedApi("com.android.text.flags.no_break_no_hyphenation_span") public static final int HYPHENATION_UNSPECIFIED = -1; // 0xffffffff
@@ -17664,16 +17665,16 @@
     field public static final int LINE_BREAK_STYLE_NONE = 0; // 0x0
     field public static final int LINE_BREAK_STYLE_NORMAL = 2; // 0x2
     field public static final int LINE_BREAK_STYLE_STRICT = 3; // 0x3
-    field public static final int LINE_BREAK_STYLE_UNSPECIFIED = -1; // 0xffffffff
+    field @FlaggedApi("com.android.text.flags.no_break_no_hyphenation_span") public static final int LINE_BREAK_STYLE_UNSPECIFIED = -1; // 0xffffffff
     field public static final int LINE_BREAK_WORD_STYLE_NONE = 0; // 0x0
     field public static final int LINE_BREAK_WORD_STYLE_PHRASE = 1; // 0x1
-    field public static final int LINE_BREAK_WORD_STYLE_UNSPECIFIED = -1; // 0xffffffff
+    field @FlaggedApi("com.android.text.flags.no_break_no_hyphenation_span") public static final int LINE_BREAK_WORD_STYLE_UNSPECIFIED = -1; // 0xffffffff
   }
 
   public static final class LineBreakConfig.Builder {
     ctor public LineBreakConfig.Builder();
     method @NonNull public android.graphics.text.LineBreakConfig build();
-    method @NonNull public android.graphics.text.LineBreakConfig.Builder merge(@NonNull android.graphics.text.LineBreakConfig);
+    method @FlaggedApi("com.android.text.flags.no_break_no_hyphenation_span") @NonNull public android.graphics.text.LineBreakConfig.Builder merge(@NonNull android.graphics.text.LineBreakConfig);
     method @FlaggedApi("com.android.text.flags.no_break_no_hyphenation_span") @NonNull public android.graphics.text.LineBreakConfig.Builder setHyphenation(int);
     method @NonNull public android.graphics.text.LineBreakConfig.Builder setLineBreakStyle(int);
     method @NonNull public android.graphics.text.LineBreakConfig.Builder setLineBreakWordStyle(int);
@@ -17698,7 +17699,7 @@
     method @NonNull public android.graphics.text.LineBreaker.Builder setHyphenationFrequency(int);
     method @NonNull public android.graphics.text.LineBreaker.Builder setIndents(@Nullable int[]);
     method @NonNull public android.graphics.text.LineBreaker.Builder setJustificationMode(int);
-    method @NonNull public android.graphics.text.LineBreaker.Builder setUseBoundsForWidth(boolean);
+    method @FlaggedApi("com.android.text.flags.use_bounds_for_width") @NonNull public android.graphics.text.LineBreaker.Builder setUseBoundsForWidth(boolean);
   }
 
   public static class LineBreaker.ParagraphConstraints {
@@ -17750,18 +17751,18 @@
     method public float getAdvance();
     method public float getAscent();
     method public float getDescent();
-    method public boolean getFakeBold(@IntRange(from=0) int);
-    method public boolean getFakeItalic(@IntRange(from=0) int);
+    method @FlaggedApi("com.android.text.flags.deprecate_fonts_xml") public boolean getFakeBold(@IntRange(from=0) int);
+    method @FlaggedApi("com.android.text.flags.deprecate_fonts_xml") public boolean getFakeItalic(@IntRange(from=0) int);
     method @NonNull public android.graphics.fonts.Font getFont(@IntRange(from=0) int);
     method @IntRange(from=0) public int getGlyphId(@IntRange(from=0) int);
     method public float getGlyphX(@IntRange(from=0) int);
     method public float getGlyphY(@IntRange(from=0) int);
-    method public float getItalicOverride(@IntRange(from=0) int);
+    method @FlaggedApi("com.android.text.flags.deprecate_fonts_xml") public float getItalicOverride(@IntRange(from=0) int);
     method public float getOffsetX();
     method public float getOffsetY();
-    method public float getWeightOverride(@IntRange(from=0) int);
+    method @FlaggedApi("com.android.text.flags.deprecate_fonts_xml") public float getWeightOverride(@IntRange(from=0) int);
     method @IntRange(from=0) public int glyphCount();
-    field public static final float NO_OVERRIDE = 1.4E-45f;
+    field @FlaggedApi("com.android.text.flags.deprecate_fonts_xml") public static final float NO_OVERRIDE = 1.4E-45f;
   }
 
   public class TextRunShaper {
@@ -18646,6 +18647,10 @@
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Boolean> FLASH_INFO_AVAILABLE;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Integer> FLASH_INFO_STRENGTH_DEFAULT_LEVEL;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Integer> FLASH_INFO_STRENGTH_MAXIMUM_LEVEL;
+    field @FlaggedApi("com.android.internal.camera.flags.camera_manual_flash_strength_control") @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Integer> FLASH_SINGLE_STRENGTH_DEFAULT_LEVEL;
+    field @FlaggedApi("com.android.internal.camera.flags.camera_manual_flash_strength_control") @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Integer> FLASH_SINGLE_STRENGTH_MAX_LEVEL;
+    field @FlaggedApi("com.android.internal.camera.flags.camera_manual_flash_strength_control") @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Integer> FLASH_TORCH_STRENGTH_DEFAULT_LEVEL;
+    field @FlaggedApi("com.android.internal.camera.flags.camera_manual_flash_strength_control") @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Integer> FLASH_TORCH_STRENGTH_MAX_LEVEL;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> HOT_PIXEL_AVAILABLE_HOT_PIXEL_MODES;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<android.hardware.camera2.params.DeviceStateSensorOrientationMap> INFO_DEVICE_STATE_SENSOR_ORIENTATION_MAP;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Integer> INFO_SUPPORTED_HARDWARE_LEVEL;
@@ -19215,6 +19220,7 @@
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> EDGE_MODE;
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> EXTENSION_STRENGTH;
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> FLASH_MODE;
+    field @FlaggedApi("com.android.internal.camera.flags.camera_manual_flash_strength_control") @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> FLASH_STRENGTH_LEVEL;
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> HOT_PIXEL_MODE;
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<android.location.Location> JPEG_GPS_LOCATION;
     field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> JPEG_ORIENTATION;
@@ -19310,6 +19316,7 @@
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> EXTENSION_STRENGTH;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> FLASH_MODE;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> FLASH_STATE;
+    field @FlaggedApi("com.android.internal.camera.flags.camera_manual_flash_strength_control") @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> FLASH_STRENGTH_LEVEL;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> HOT_PIXEL_MODE;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<android.location.Location> JPEG_GPS_LOCATION;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> JPEG_ORIENTATION;
@@ -39002,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();
@@ -39009,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();
@@ -39040,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);
@@ -39144,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();
@@ -39171,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);
@@ -47891,8 +47904,8 @@
   }
 
   @FlaggedApi("com.android.text.flags.no_break_no_hyphenation_span") public class LineBreakConfigSpan {
-    ctor public LineBreakConfigSpan(@NonNull android.graphics.text.LineBreakConfig);
-    method @NonNull public android.graphics.text.LineBreakConfig getLineBreakConfig();
+    ctor @FlaggedApi("com.android.text.flags.no_break_no_hyphenation_span") public LineBreakConfigSpan(@NonNull android.graphics.text.LineBreakConfig);
+    method @FlaggedApi("com.android.text.flags.no_break_no_hyphenation_span") @NonNull public android.graphics.text.LineBreakConfig getLineBreakConfig();
   }
 
   @FlaggedApi("com.android.text.flags.no_break_no_hyphenation_span") public static final class LineBreakConfigSpan.NoHyphenationSpan extends android.text.style.LineBreakConfigSpan {
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 ad65806..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 @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
@@ -4529,6 +4529,7 @@
     method @RequiresPermission(android.Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS) public void setBrightnessConfiguration(android.hardware.display.BrightnessConfiguration);
     method @RequiresPermission(android.Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS) public void setBrightnessConfigurationForDisplay(@NonNull android.hardware.display.BrightnessConfiguration, @NonNull String);
     method @Deprecated @RequiresPermission(android.Manifest.permission.CONTROL_DISPLAY_SATURATION) public void setSaturationLevel(float);
+    field public static final int VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 128; // 0x80
     field public static final int VIRTUAL_DISPLAY_FLAG_STEAL_TOP_FOCUS_DISABLED = 65536; // 0x10000
     field public static final int VIRTUAL_DISPLAY_FLAG_TRUSTED = 1024; // 0x400
   }
@@ -9631,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;
   }
 
 }
@@ -9842,7 +9843,6 @@
     field public static final int BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA = 1; // 0x1
     field public static final int BUGREPORT_MODE_FULL = 0; // 0x0
     field public static final int BUGREPORT_MODE_INTERACTIVE = 1; // 0x1
-    field public static final int BUGREPORT_MODE_ONBOARDING = 7; // 0x7
     field public static final int BUGREPORT_MODE_REMOTE = 2; // 0x2
     field public static final int BUGREPORT_MODE_TELEPHONY = 4; // 0x4
     field public static final int BUGREPORT_MODE_WEAR = 3; // 0x3
@@ -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/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 4f45691..3bf2cca 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -24,6 +24,7 @@
 import android.Manifest;
 import android.annotation.ColorInt;
 import android.annotation.DrawableRes;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
@@ -3982,6 +3983,7 @@
      *         the order from most recent to least recent.
      */
     @NonNull
+    @FlaggedApi(Flags.FLAG_APP_START_INFO)
     public List<ApplicationStartInfo> getHistoricalProcessStartReasons(
             @IntRange(from = 0) int maxNum) {
         try {
@@ -4012,6 +4014,7 @@
      */
     @NonNull
     @SystemApi
+    @FlaggedApi(Flags.FLAG_APP_START_INFO)
     @RequiresPermission(Manifest.permission.DUMP)
     public List<ApplicationStartInfo> getExternalHistoricalProcessStartReasons(
             @NonNull String packageName, @IntRange(from = 0) int maxNum) {
@@ -4044,6 +4047,7 @@
      *
      * @throws IllegalArgumentException if executor or listener are null.
      */
+    @FlaggedApi(Flags.FLAG_APP_START_INFO)
     public void setApplicationStartInfoCompletionListener(@NonNull final Executor executor,
             @NonNull final Consumer<ApplicationStartInfo> listener) {
         Preconditions.checkNotNull(executor, "executor cannot be null");
@@ -4065,6 +4069,7 @@
     /**
      * Removes the callback set by {@link #setApplicationStartInfoCompletionListener} if there is one.
      */
+    @FlaggedApi(Flags.FLAG_APP_START_INFO)
     public void clearApplicationStartInfoCompletionListener() {
         try {
             getService().clearApplicationStartInfoCompleteListener(mContext.getUserId());
@@ -4089,6 +4094,7 @@
      *                    Will thow {@link java.lang.IllegalArgumentException} if not in range.
      * @param timestampNs Clock monotonic time in nanoseconds of event to be recorded.
      */
+    @FlaggedApi(Flags.FLAG_APP_START_INFO)
     public void addStartInfoTimestamp(@IntRange(
             from = ApplicationStartInfo.START_TIMESTAMP_RESERVED_RANGE_DEVELOPER_START,
             to = ApplicationStartInfo.START_TIMESTAMP_RESERVED_RANGE_DEVELOPER) int key,
diff --git a/core/java/android/app/Application.java b/core/java/android/app/Application.java
index 2767b43..6a50e74 100644
--- a/core/java/android/app/Application.java
+++ b/core/java/android/app/Application.java
@@ -616,9 +616,8 @@
             Log.v(TAG, "getAutofillClient(): null on super, trying to find activity thread");
         }
         // Okay, ppl use the application context when they should not. This breaks
-        // autofill among other things. We pick the focused activity since autofill
-        // interacts only with the currently focused activity and we need the fill
-        // client only if a call comes from the focused activity. Sigh...
+        // autofill among other things. Below is a mitigation to find the top resumed
+        // activity.
         final ActivityThread activityThread = ActivityThread.currentActivityThread();
         if (activityThread == null) {
             return null;
@@ -634,16 +633,27 @@
             if (activity == null) {
                 continue;
             }
+            if (record.isTopResumedActivity) {
+                if (android.view.autofill.Helper.sVerbose) {
+                    Log.v(TAG, "getAutofillClient(): found top resumed activity for " + this +
+                            ": " + activity);
+                }
+                return activity.getAutofillClient();
+            }
+            // As a back up option, we pick the focused activity since autofill interacts only
+            // with the currently focused activity and we need the fill client only if a call
+            // comes from the focused activity.
             if (activity.getWindow().getDecorView().hasFocus()) {
                 if (android.view.autofill.Helper.sVerbose) {
-                    Log.v(TAG, "getAutofillClient(): found activity for " + this + ": " + activity);
+                    Log.v(TAG, "getAutofillClient(): found focused activity for " + this +
+                            ": " + activity);
                 }
                 return activity.getAutofillClient();
             }
         }
         if (android.view.autofill.Helper.sVerbose) {
             Log.v(TAG, "getAutofillClient(): none of the " + activityCount + " activities on "
-                    + this + " have focus");
+                    + this + " are top resumed nor have focus");
         }
         return null;
     }
diff --git a/core/java/android/app/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java
index f5fb6ed..a6a57cd 100644
--- a/core/java/android/app/ApplicationStartInfo.java
+++ b/core/java/android/app/ApplicationStartInfo.java
@@ -16,6 +16,7 @@
 
 package android.app;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -37,6 +38,7 @@
 /**
  * Provide information related to a processes startup.
  */
+@FlaggedApi(Flags.FLAG_APP_START_INFO)
 public final class ApplicationStartInfo implements Parcelable {
 
     /**
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index 7f38b27..ec5effd 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -98,6 +98,7 @@
     ParceledListSlice getNotificationChannelGroupsForPackage(String pkg, int uid, boolean includeDeleted);
     NotificationChannelGroup getNotificationChannelGroupForPackage(String groupId, String pkg, int uid);
     NotificationChannelGroup getPopulatedNotificationChannelGroupForPackage(String pkg, int uid, String groupId, boolean includeDeleted);
+    ParceledListSlice getRecentBlockedNotificationChannelGroupsForPackage(String pkg, int uid);
     void updateNotificationChannelGroupForPackage(String pkg, int uid, in NotificationChannelGroup group);
     void updateNotificationChannelForPackage(String pkg, int uid, in NotificationChannel channel);
     void unlockNotificationChannel(String pkg, int uid, String channelId);
diff --git a/core/java/android/app/UiAutomationConnection.java b/core/java/android/app/UiAutomationConnection.java
index 6f4abfd..6a03c17 100644
--- a/core/java/android/app/UiAutomationConnection.java
+++ b/core/java/android/app/UiAutomationConnection.java
@@ -207,9 +207,10 @@
         final long identity = Binder.clearCallingIdentity();
         try {
             if (rotation == UiAutomation.ROTATION_UNFREEZE) {
-                mWindowManager.thawRotation();
+                mWindowManager.thawRotation(/* caller= */ "UiAutomationConnection#setRotation");
             } else {
-                mWindowManager.freezeRotation(rotation);
+                mWindowManager.freezeRotation(rotation,
+                        /* caller= */ "UiAutomationConnection#setRotation");
             }
             return true;
         } catch (RemoteException re) {
@@ -615,11 +616,13 @@
             if (mInitialFrozenRotation != INITIAL_FROZEN_ROTATION_UNSPECIFIED) {
                 // Calling out with a lock held is fine since if the system
                 // process is gone the client calling in will be killed.
-                mWindowManager.freezeRotation(mInitialFrozenRotation);
+                mWindowManager.freezeRotation(mInitialFrozenRotation,
+                        /* caller= */ "UiAutomationConnection#restoreRotationStateLocked");
             } else {
                 // Calling out with a lock held is fine since if the system
                 // process is gone the client calling in will be killed.
-                mWindowManager.thawRotation();
+                mWindowManager.thawRotation(
+                        /* caller= */ "UiAutomationConnection#restoreRotationStateLocked");
             }
         } catch (RemoteException re) {
             /* ignore */
diff --git a/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig
new file mode 100644
index 0000000..2076e85
--- /dev/null
+++ b/core/java/android/app/activity_manager.aconfig
@@ -0,0 +1,8 @@
+package: "android.app"
+
+flag {
+     namespace: "system_performance"
+     name: "app_start_info"
+     description: "Control collecting of ApplicationStartInfo records and APIs."
+     bug: "247814855"
+}
\ No newline at end of file
diff --git a/core/java/android/app/assist/AssistStructure.java b/core/java/android/app/assist/AssistStructure.java
index ed0f872..15bd1dc 100644
--- a/core/java/android/app/assist/AssistStructure.java
+++ b/core/java/android/app/assist/AssistStructure.java
@@ -22,8 +22,10 @@
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.service.autofill.FillRequest;
+import android.text.InputType;
 import android.text.Spanned;
 import android.text.TextUtils;
+import android.util.ArrayMap;
 import android.util.Log;
 import android.util.Pair;
 import android.view.View;
@@ -2452,7 +2454,7 @@
                     + node.getTextStyle());
             Log.i(TAG, prefix + "  Text color fg: #" + Integer.toHexString(node.getTextColor())
                     + ", bg: #" + Integer.toHexString(node.getTextBackgroundColor()));
-            Log.i(TAG, prefix + "  Input type: " + node.getInputType());
+            Log.i(TAG, prefix + "  Input type: " + getInputTypeString(node.getInputType()));
             Log.i(TAG, prefix + "  Resource id: " + node.getTextIdEntry());
         }
         String webDomain = node.getWebDomain();
@@ -2664,4 +2666,33 @@
             return new AssistStructure[size];
         }
     };
+
+    private static final ArrayMap<Integer, String> INPUT_TYPE_VARIATIONS = new ArrayMap<>();
+    static {
+        INPUT_TYPE_VARIATIONS.put(InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS, "EmailSubject");
+        INPUT_TYPE_VARIATIONS.put(InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS, "PostalAddress");
+        INPUT_TYPE_VARIATIONS.put(InputType.TYPE_TEXT_VARIATION_PERSON_NAME, "PersonName");
+        INPUT_TYPE_VARIATIONS.put(InputType.TYPE_TEXT_VARIATION_PASSWORD, "Password");
+        INPUT_TYPE_VARIATIONS.put(InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD, "VisiblePassword");
+        INPUT_TYPE_VARIATIONS.put(InputType.TYPE_TEXT_VARIATION_URI, "URI");
+        INPUT_TYPE_VARIATIONS.put(InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS, "WebEmailAddress");
+        INPUT_TYPE_VARIATIONS.put(InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD, "WebPassword");
+        INPUT_TYPE_VARIATIONS.put(InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE, "LongMessage");
+        INPUT_TYPE_VARIATIONS.put(InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE, "ShortMessage");
+        INPUT_TYPE_VARIATIONS.put(InputType.TYPE_TEXT_FLAG_MULTI_LINE, "MultiLine");
+        INPUT_TYPE_VARIATIONS.put(InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE, "ImeMultiLine");
+        INPUT_TYPE_VARIATIONS.put(InputType.TYPE_TEXT_VARIATION_FILTER, "Filter");
+    }
+
+    private static String getInputTypeString(int inputType) {
+        StringBuilder sb = new StringBuilder();
+        sb.append(inputType);
+        sb.append("(class=").append(inputType & InputType.TYPE_MASK_CLASS).append(')');
+        for (int variation : INPUT_TYPE_VARIATIONS.keySet()) {
+            if ((variation & inputType) == variation) {
+                sb.append('|').append(INPUT_TYPE_VARIATIONS.get(variation));
+            }
+        }
+        return sb.toString();
+    }
 }
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/database/sqlite/package.html b/core/java/android/database/sqlite/package.html
index 2c36646..f2df536 100644
--- a/core/java/android/database/sqlite/package.html
+++ b/core/java/android/database/sqlite/package.html
@@ -15,7 +15,7 @@
 <a href="{@docRoot}studio/command-line/sqlite3.html">sqlite3</a> command-line
 database tool. On your development machine, run the tool from the
 <code>platform-tools/</code> folder of your SDK. On the emulator, run the tool
-with adb shell, for example, <code>adb -e shell sqlite3</code>.
+with adb shell, for example, <code>adb shell sqlite3</code>.
 
 <p>The version of SQLite depends on the version of Android. See the following table:
 <table style="width:auto;">
@@ -42,15 +42,19 @@
 
 <ul>
   <li>If available, use the sqlite3 tool, for example:
-    <code>adb -e shell sqlite3 --version</code>.</li>
+    <code>adb shell sqlite3 --version</code>.</li>
   <li>Create and query an in-memory database as shown in the following code sample:
     <pre>
     String query = "select sqlite_version() AS sqlite_version";
     SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(":memory:", null);
     Cursor cursor = db.rawQuery(query, null);
     String sqliteVersion = "";
-    if (cursor.moveToNext()) {
-        sqliteVersion = cursor.getString(0);
+    try {
+        if (cursor.moveToNext()) {
+            sqliteVersion = cursor.getString(0);
+        }
+    } finally {
+        cursor.close();
     }</pre>
   </li>
 </ul>
diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java
index 943f0c4..89f889f 100644
--- a/core/java/android/hardware/camera2/CameraCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraCharacteristics.java
@@ -1407,6 +1407,74 @@
             new Key<Integer>("android.flash.info.strengthDefaultLevel", int.class);
 
     /**
+     * <p>Maximum flash brightness level for manual flash control in SINGLE mode.</p>
+     * <p>Maximum flash brightness level in camera capture mode and
+     * {@link CaptureRequest#FLASH_MODE android.flash.mode} set to SINGLE.
+     * Value will be &gt; 1 if the manual flash strength control feature is supported,
+     * otherwise the value will be equal to 1.
+     * Note that this level is just a number of supported levels (the granularity of control).
+     * There is no actual physical power units tied to this level.</p>
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     *
+     * @see CaptureRequest#FLASH_MODE
+     */
+    @PublicKey
+    @NonNull
+    @FlaggedApi(Flags.FLAG_CAMERA_MANUAL_FLASH_STRENGTH_CONTROL)
+    public static final Key<Integer> FLASH_SINGLE_STRENGTH_MAX_LEVEL =
+            new Key<Integer>("android.flash.singleStrengthMaxLevel", int.class);
+
+    /**
+     * <p>Default flash brightness level for manual flash control in SINGLE mode.</p>
+     * <p>If flash unit is available this will be greater than or equal to 1 and less
+     * or equal to <code>android.flash.info.singleStrengthMaxLevel</code>.
+     * Note for devices that do not support the manual flash strength control
+     * feature, this level will always be equal to 1.</p>
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     */
+    @PublicKey
+    @NonNull
+    @FlaggedApi(Flags.FLAG_CAMERA_MANUAL_FLASH_STRENGTH_CONTROL)
+    public static final Key<Integer> FLASH_SINGLE_STRENGTH_DEFAULT_LEVEL =
+            new Key<Integer>("android.flash.singleStrengthDefaultLevel", int.class);
+
+    /**
+     * <p>Maximum flash brightness level for manual flash control in TORCH mode</p>
+     * <p>Maximum flash brightness level in camera capture mode and
+     * {@link CaptureRequest#FLASH_MODE android.flash.mode} set to TORCH.
+     * Value will be &gt; 1 if the manual flash strength control feature is supported,
+     * otherwise the value will be equal to 1.</p>
+     * <p>Note that this level is just a number of supported levels(the granularity of control).
+     * There is no actual physical power units tied to this level.
+     * There is no relation between android.flash.info.torchStrengthMaxLevel and
+     * android.flash.info.singleStrengthMaxLevel i.e. the ratio of
+     * android.flash.info.torchStrengthMaxLevel:android.flash.info.singleStrengthMaxLevel
+     * is not guaranteed to be the ratio of actual brightness.</p>
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     *
+     * @see CaptureRequest#FLASH_MODE
+     */
+    @PublicKey
+    @NonNull
+    @FlaggedApi(Flags.FLAG_CAMERA_MANUAL_FLASH_STRENGTH_CONTROL)
+    public static final Key<Integer> FLASH_TORCH_STRENGTH_MAX_LEVEL =
+            new Key<Integer>("android.flash.torchStrengthMaxLevel", int.class);
+
+    /**
+     * <p>Default flash brightness level for manual flash control in TORCH mode</p>
+     * <p>If flash unit is available this will be greater than or equal to 1 and less
+     * or equal to android.flash.info.torchStrengthMaxLevel.
+     * Note for the devices that do not support the manual flash strength control feature,
+     * this level will always be equal to 1.</p>
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     */
+    @PublicKey
+    @NonNull
+    @FlaggedApi(Flags.FLAG_CAMERA_MANUAL_FLASH_STRENGTH_CONTROL)
+    public static final Key<Integer> FLASH_TORCH_STRENGTH_DEFAULT_LEVEL =
+            new Key<Integer>("android.flash.torchStrengthDefaultLevel", int.class);
+
+    /**
      * <p>List of hot pixel correction modes for {@link CaptureRequest#HOT_PIXEL_MODE android.hotPixel.mode} that are supported by this
      * camera device.</p>
      * <p>FULL mode camera devices will always support FAST.</p>
diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java
index dd32384..9421359 100644
--- a/core/java/android/hardware/camera2/CaptureRequest.java
+++ b/core/java/android/hardware/camera2/CaptureRequest.java
@@ -2654,6 +2654,46 @@
             new Key<Integer>("android.flash.mode", int.class);
 
     /**
+     * <p>Flash strength level to be used when manual flash control is active.</p>
+     * <p>Flash strength level to use in capture mode i.e. when the applications control
+     * flash with either SINGLE or TORCH mode.</p>
+     * <p>Use android.flash.info.singleStrengthMaxLevel and
+     * android.flash.info.torchStrengthMaxLevel to check whether the device supports
+     * flash strength control or not.
+     * If the values of android.flash.info.singleStrengthMaxLevel and
+     * android.flash.info.torchStrengthMaxLevel are greater than 1,
+     * then the device supports manual flash strength control.</p>
+     * <p>If the {@link CaptureRequest#FLASH_MODE android.flash.mode} <code>==</code> TORCH the value must be &gt;= 1
+     * and &lt;= android.flash.info.torchStrengthMaxLevel.
+     * If the application doesn't set the key and
+     * android.flash.info.torchStrengthMaxLevel &gt; 1,
+     * then the flash will be fired at the default level set by HAL in
+     * android.flash.info.torchStrengthDefaultLevel.
+     * If the {@link CaptureRequest#FLASH_MODE android.flash.mode} <code>==</code> SINGLE, then the value must be &gt;= 1
+     * and &lt;= android.flash.info.singleStrengthMaxLevel.
+     * If the application does not set this key and
+     * android.flash.info.singleStrengthMaxLevel &gt; 1,
+     * then the flash will be fired at the default level set by HAL
+     * in android.flash.info.singleStrengthDefaultLevel.
+     * If {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is set to any of ON_AUTO_FLASH, ON_ALWAYS_FLASH,
+     * ON_AUTO_FLASH_REDEYE, ON_EXTERNAL_FLASH values, then the strengthLevel will be ignored.</p>
+     * <p><b>Range of valid values:</b><br>
+     * <code>[1-android.flash.info.torchStrengthMaxLevel]</code> when the {@link CaptureRequest#FLASH_MODE android.flash.mode} is
+     * set to TORCH;
+     * <code>[1-android.flash.info.singleStrengthMaxLevel]</code> when the {@link CaptureRequest#FLASH_MODE android.flash.mode} is
+     * set to SINGLE</p>
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     *
+     * @see CaptureRequest#CONTROL_AE_MODE
+     * @see CaptureRequest#FLASH_MODE
+     */
+    @PublicKey
+    @NonNull
+    @FlaggedApi(Flags.FLAG_CAMERA_MANUAL_FLASH_STRENGTH_CONTROL)
+    public static final Key<Integer> FLASH_STRENGTH_LEVEL =
+            new Key<Integer>("android.flash.strengthLevel", int.class);
+
+    /**
      * <p>Operational mode for hot pixel correction.</p>
      * <p>Hotpixel correction interpolates out, or otherwise removes, pixels
      * that do not accurately measure the incoming light (i.e. pixels that
diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java
index 9a80015..4606e5b 100644
--- a/core/java/android/hardware/camera2/CaptureResult.java
+++ b/core/java/android/hardware/camera2/CaptureResult.java
@@ -2940,6 +2940,46 @@
             new Key<Integer>("android.flash.state", int.class);
 
     /**
+     * <p>Flash strength level to be used when manual flash control is active.</p>
+     * <p>Flash strength level to use in capture mode i.e. when the applications control
+     * flash with either SINGLE or TORCH mode.</p>
+     * <p>Use android.flash.info.singleStrengthMaxLevel and
+     * android.flash.info.torchStrengthMaxLevel to check whether the device supports
+     * flash strength control or not.
+     * If the values of android.flash.info.singleStrengthMaxLevel and
+     * android.flash.info.torchStrengthMaxLevel are greater than 1,
+     * then the device supports manual flash strength control.</p>
+     * <p>If the {@link CaptureRequest#FLASH_MODE android.flash.mode} <code>==</code> TORCH the value must be &gt;= 1
+     * and &lt;= android.flash.info.torchStrengthMaxLevel.
+     * If the application doesn't set the key and
+     * android.flash.info.torchStrengthMaxLevel &gt; 1,
+     * then the flash will be fired at the default level set by HAL in
+     * android.flash.info.torchStrengthDefaultLevel.
+     * If the {@link CaptureRequest#FLASH_MODE android.flash.mode} <code>==</code> SINGLE, then the value must be &gt;= 1
+     * and &lt;= android.flash.info.singleStrengthMaxLevel.
+     * If the application does not set this key and
+     * android.flash.info.singleStrengthMaxLevel &gt; 1,
+     * then the flash will be fired at the default level set by HAL
+     * in android.flash.info.singleStrengthDefaultLevel.
+     * If {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is set to any of ON_AUTO_FLASH, ON_ALWAYS_FLASH,
+     * ON_AUTO_FLASH_REDEYE, ON_EXTERNAL_FLASH values, then the strengthLevel will be ignored.</p>
+     * <p><b>Range of valid values:</b><br>
+     * <code>[1-android.flash.info.torchStrengthMaxLevel]</code> when the {@link CaptureRequest#FLASH_MODE android.flash.mode} is
+     * set to TORCH;
+     * <code>[1-android.flash.info.singleStrengthMaxLevel]</code> when the {@link CaptureRequest#FLASH_MODE android.flash.mode} is
+     * set to SINGLE</p>
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     *
+     * @see CaptureRequest#CONTROL_AE_MODE
+     * @see CaptureRequest#FLASH_MODE
+     */
+    @PublicKey
+    @NonNull
+    @FlaggedApi(Flags.FLAG_CAMERA_MANUAL_FLASH_STRENGTH_CONTROL)
+    public static final Key<Integer> FLASH_STRENGTH_LEVEL =
+            new Key<Integer>("android.flash.strengthLevel", int.class);
+
+    /**
      * <p>Operational mode for hot pixel correction.</p>
      * <p>Hotpixel correction interpolates out, or otherwise removes, pixels
      * that do not accurately measure the incoming light (i.e. pixels that
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index f0c87a1..990ebc5 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -27,6 +27,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
 import android.annotation.TestApi;
@@ -364,11 +365,20 @@
 
     /**
      * Virtual display flag: Indicates that the orientation of this display device is coupled to
-     * the rotation of its associated logical display.
+     * the orientation of its associated logical display.
+     * <p>
+     * The flag should not be set when the physical display is mounted in a fixed orientation
+     * such as on a desk. Without this flag, display manager will apply a coordinate transformation
+     * such as a scale and translation to letterbox or pillarbox format under the assumption that
+     * the physical orientation of the display is invariant. With this flag set, the content will
+     * rotate to fill in the space of the display, as it does on the internal device display.
+     * </p>
      *
      * @see #createVirtualDisplay
      * @hide
      */
+    @SuppressLint("UnflaggedApi")
+    @SystemApi
     public static final int VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 1 << 7;
 
     /**
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/hardware/radio/flags.aconfig b/core/java/android/hardware/radio/flags.aconfig
new file mode 100644
index 0000000..dbc1a4b
--- /dev/null
+++ b/core/java/android/hardware/radio/flags.aconfig
@@ -0,0 +1,8 @@
+package: "android.hardware.radio"
+
+flag {
+    name: "hd_radio_improved"
+    namespace: "car_framework"
+    description: "Feature flag for improved HD radio support with less vendor extensions"
+    bug: "280300929"
+}
diff --git a/core/java/android/net/network-policy-restrictions.md b/core/java/android/net/network-policy-restrictions.md
index 04c658c..20f3d74 100644
--- a/core/java/android/net/network-policy-restrictions.md
+++ b/core/java/android/net/network-policy-restrictions.md
@@ -29,8 +29,8 @@
 | **DS**  |  *AL* |  ok  | blk   |  ok  |  ok   |
 | **ON**  | *!AL* | blk  | blk   | blk  | blk   |
 |         |  *DL* | blk  | blk   | blk  | blk   |
-| **DS**  |  *AL* | blk  | blk   |  ok  |  ok   |
-| **OFF** | *!AL* | blk  | blk   |  ok  |  ok   |
+| **DS**  |  *AL* |  ok  | blk   |  ok  |  ok   |
+| **OFF** | *!AL* |  ok  | blk   |  ok  |  ok   |
 |         |  *DL* | blk  | blk   | blk  | blk   |
 
 
diff --git a/core/java/android/os/BinderProxy.java b/core/java/android/os/BinderProxy.java
index ada5532..f817fb8 100644
--- a/core/java/android/os/BinderProxy.java
+++ b/core/java/android/os/BinderProxy.java
@@ -32,7 +32,9 @@
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -613,15 +615,35 @@
      */
     public native boolean transactNative(int code, Parcel data, Parcel reply,
             int flags) throws RemoteException;
+
+    /* This list is to hold strong reference to the death recipients that are waiting for the death
+     * of binder that this proxy references. Previously, the death recipients were strongy
+     * referenced from JNI, but that can cause memory leak (b/298374304) when the application has a
+     * strong reference from the death recipient to the proxy object. The JNI reference is now weak.
+     * And this strong reference is to keep death recipients at least until the proxy is GC'ed. */
+    private List<DeathRecipient> mDeathRecipients = Collections.synchronizedList(new ArrayList<>());
+
     /**
      * See {@link IBinder#linkToDeath(DeathRecipient, int)}
      */
-    public native void linkToDeath(DeathRecipient recipient, int flags)
-            throws RemoteException;
+    public void linkToDeath(DeathRecipient recipient, int flags)
+            throws RemoteException {
+        linkToDeathNative(recipient, flags);
+        mDeathRecipients.add(recipient);
+    }
+
     /**
      * See {@link IBinder#unlinkToDeath}
      */
-    public native boolean unlinkToDeath(DeathRecipient recipient, int flags);
+    public boolean unlinkToDeath(DeathRecipient recipient, int flags) {
+        mDeathRecipients.remove(recipient);
+        return unlinkToDeathNative(recipient, flags);
+    }
+
+    private native void linkToDeathNative(DeathRecipient recipient, int flags)
+            throws RemoteException;
+
+    private native boolean unlinkToDeathNative(DeathRecipient recipient, int flags);
 
     /**
      * Perform a dump on the remote object
diff --git a/core/java/android/os/BugreportParams.java b/core/java/android/os/BugreportParams.java
index f10467f..47ad72f 100644
--- a/core/java/android/os/BugreportParams.java
+++ b/core/java/android/os/BugreportParams.java
@@ -124,6 +124,8 @@
 
     /**
      * Options for a lightweight bugreport intended to be taken for onboarding-related flows.
+     *
+     * @hide
      */
     public static final int BUGREPORT_MODE_ONBOARDING = IDumpstate.BUGREPORT_MODE_ONBOARDING;
 
diff --git a/core/java/android/security/FileIntegrityManager.java b/core/java/android/security/FileIntegrityManager.java
index 2dbb5da..dae3202 100644
--- a/core/java/android/security/FileIntegrityManager.java
+++ b/core/java/android/security/FileIntegrityManager.java
@@ -76,28 +76,38 @@
      * Enables fs-verity to the owned file under the calling app's private directory. It always uses
      * the common configuration, i.e. SHA-256 digest algorithm, 4K block size, and without salt.
      *
-     * The operation can only succeed when the file is not opened as writable by any process.
+     * <p>For enabling fs-verity to succeed, the device must support fs-verity, the file must be
+     * writable by the app and not already have fs-verity enabled, and the file must not currently
+     * be open for writing by any process. To check whether the device supports fs-verity, use
+     * {@link #isApkVeritySupported()}.
      *
-     * It takes O(file size) time to build the underlying data structure for continuous
+     * <p>It takes O(file size) time to build the underlying data structure for continuous
      * verification. The operation is atomic, i.e. it's either enabled or not, even in case of
      * power failure during or after the call.
      *
-     * Note for the API users: When the file's authenticity is crucial, the app typical needs to
+     * <p>Note for the API users: When the file's authenticity is crucial, the app typical needs to
      * perform a signature check by itself before using the file. The signature is often delivered
      * as a separate file and stored next to the targeting file in the filesystem. The public key of
      * the signer (normally the same app developer) can be put in the APK, and the app can use the
      * public key to verify the signature to the file's actual fs-verity digest (from {@link
-     * #getFsVerityDigest}) before using the file. The exact format is not prescribed by the
+     * #getFsVerityDigest(File)}) before using the file. The exact format is not prescribed by the
      * framework. App developers may choose to use common practices like JCA for the signing and
      * verification, or their own preferred approach.
      *
-     * @param file The file to enable fs-verity. It should be an absolute path.
+     * @param file The file to enable fs-verity. It must represent an absolute path.
+     * @throws IllegalArgumentException If the provided file is not an absolute path.
+     * @throws IOException If the operation failed.
      *
      * @see <a href="https://www.kernel.org/doc/html/next/filesystems/fsverity.html">Kernel doc</a>
      */
     @FlaggedApi(Flags.FLAG_FSVERITY_API)
     public void setupFsVerity(@NonNull File file) throws IOException {
         if (!file.isAbsolute()) {
+            // fs-verity is to be enabled by installd, which enforces the validation to the
+            // (untrusted) file path passed from here. To make this less error prone, installd
+            // accepts only absolute path. When a relative path is provided, we fail with an
+            // explicit exception to help developers understand the requirement to use an absolute
+            // path.
             throw new IllegalArgumentException("Expect an absolute path");
         }
         IFsveritySetupAuthToken authToken;
@@ -121,11 +131,12 @@
     }
 
     /**
-     * Returns the fs-verity digest for the owned file under the calling app's
-     * private directory, or null when the file does not have fs-verity enabled.
+     * Returns the fs-verity digest for the owned file under the calling app's private directory, or
+     * null when the file does not have fs-verity enabled (including when fs-verity is not supported
+     * on older devices).
      *
      * @param file The file to measure the fs-verity digest.
-     * @return The fs-verity digeset in byte[], null if none.
+     * @return The fs-verity digest in byte[], null if none.
      * @see <a href="https://www.kernel.org/doc/html/next/filesystems/fsverity.html">Kernel doc</a>
      */
     @FlaggedApi(Flags.FLAG_FSVERITY_API)
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/text/TextUtils.java b/core/java/android/text/TextUtils.java
index 447c3bc..4e3deb6 100644
--- a/core/java/android/text/TextUtils.java
+++ b/core/java/android/text/TextUtils.java
@@ -2495,8 +2495,14 @@
 
                 // Even if the argument name is `ellipsizeDip`, the unit of this argument is pixels.
                 final int charCount = (int) ((ellipsizeDip + 0.5f) / assumedCharWidthInPx);
-                return TextUtils.trimToSize(gettingCleaned.toString(), charCount)
-                        + getEllipsisString(TruncateAt.END);
+
+                final String text = gettingCleaned.toString();
+                if (TextUtils.isEmpty(text) || text.length() <= charCount) {
+                    return text;
+                } else {
+                    return TextUtils.trimToSize(text, charCount)
+                            + getEllipsisString(TruncateAt.END);
+                }
             } else {
                 // Truncate
                 final TextPaint paint = new TextPaint();
diff --git a/core/java/android/text/style/LineBreakConfigSpan.java b/core/java/android/text/style/LineBreakConfigSpan.java
index b8033a9..25c1db4d 100644
--- a/core/java/android/text/style/LineBreakConfigSpan.java
+++ b/core/java/android/text/style/LineBreakConfigSpan.java
@@ -35,6 +35,7 @@
      * Construct a new {@link LineBreakConfigSpan}
      * @param lineBreakConfig a line break config
      */
+    @FlaggedApi(FLAG_NO_BREAK_NO_HYPHENATION_SPAN)
     public LineBreakConfigSpan(@NonNull LineBreakConfig lineBreakConfig) {
         mLineBreakConfig = lineBreakConfig;
     }
@@ -43,6 +44,7 @@
      * Gets an associated line break config.
      * @return associated line break config.
      */
+    @FlaggedApi(FLAG_NO_BREAK_NO_HYPHENATION_SPAN)
     public @NonNull LineBreakConfig getLineBreakConfig() {
         return mLineBreakConfig;
     }
diff --git a/core/java/android/view/DisplayEventReceiver.java b/core/java/android/view/DisplayEventReceiver.java
index a46136a..31d759e 100644
--- a/core/java/android/view/DisplayEventReceiver.java
+++ b/core/java/android/view/DisplayEventReceiver.java
@@ -264,6 +264,16 @@
     }
 
     /**
+     * Called when a display hotplug event with connection error is received.
+     *
+     * @param timestampNanos The timestamp of the event, in the {@link System#nanoTime()}
+     * timebase.
+     * @param connectionError the hotplug connection error code.
+     */
+    public void onHotplugConnectionError(long timestampNanos, int connectionError) {
+    }
+
+    /**
      * Called when a display mode changed event is received.
      *
      * @param timestampNanos The timestamp of the event, in the {@link System#nanoTime()}
@@ -345,6 +355,11 @@
         onHotplug(timestampNanos, physicalDisplayId, connected);
     }
 
+    @SuppressWarnings("unused")
+    private void dispatchHotplugConnectionError(long timestampNanos, int connectionError) {
+        onHotplugConnectionError(timestampNanos, connectionError);
+    }
+
     // Called from native code.
     @SuppressWarnings("unused")
     private void dispatchModeChanged(long timestampNanos, long physicalDisplayId, int modeId,
diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java
index 512f4f2..981911e 100644
--- a/core/java/android/view/DisplayInfo.java
+++ b/core/java/android/view/DisplayInfo.java
@@ -16,6 +16,7 @@
 
 package android.view;
 
+import static android.view.Display.Mode.INVALID_MODE_ID;
 import static android.view.DisplayInfoProto.APP_HEIGHT;
 import static android.view.DisplayInfoProto.APP_WIDTH;
 import static android.view.DisplayInfoProto.CUTOUT;
@@ -200,6 +201,11 @@
     public int defaultModeId;
 
     /**
+     * The user preferred display mode.
+     */
+    public int userPreferredModeId = INVALID_MODE_ID;
+
+    /**
      * The supported modes of this display.
      */
     public Display.Mode[] supportedModes = Display.Mode.EMPTY_ARRAY;
@@ -420,6 +426,7 @@
                 && modeId == other.modeId
                 && renderFrameRate == other.renderFrameRate
                 && defaultModeId == other.defaultModeId
+                && userPreferredModeId == other.userPreferredModeId
                 && Arrays.equals(supportedModes, other.supportedModes)
                 && colorMode == other.colorMode
                 && Arrays.equals(supportedColorModes, other.supportedColorModes)
@@ -478,6 +485,7 @@
         modeId = other.modeId;
         renderFrameRate = other.renderFrameRate;
         defaultModeId = other.defaultModeId;
+        userPreferredModeId = other.userPreferredModeId;
         supportedModes = Arrays.copyOf(other.supportedModes, other.supportedModes.length);
         colorMode = other.colorMode;
         supportedColorModes = Arrays.copyOf(
@@ -530,6 +538,7 @@
         modeId = source.readInt();
         renderFrameRate = source.readFloat();
         defaultModeId = source.readInt();
+        userPreferredModeId = source.readInt();
         int nModes = source.readInt();
         supportedModes = new Display.Mode[nModes];
         for (int i = 0; i < nModes; i++) {
@@ -596,6 +605,7 @@
         dest.writeInt(modeId);
         dest.writeFloat(renderFrameRate);
         dest.writeInt(defaultModeId);
+        dest.writeInt(userPreferredModeId);
         dest.writeInt(supportedModes.length);
         for (int i = 0; i < supportedModes.length; i++) {
             supportedModes[i].writeToParcel(dest, flags);
@@ -832,9 +842,12 @@
         sb.append(presentationDeadlineNanos);
         sb.append(", mode ");
         sb.append(modeId);
+        sb.append(", renderFrameRate ");
         sb.append(renderFrameRate);
         sb.append(", defaultMode ");
         sb.append(defaultModeId);
+        sb.append(", userPreferredModeId ");
+        sb.append(userPreferredModeId);
         sb.append(", modes ");
         sb.append(Arrays.toString(supportedModes));
         sb.append(", hdrCapabilities ");
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index d3b7a5b..cccac95 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -316,14 +316,14 @@
      * android.view.Display#DEFAULT_DISPLAY} and given rotation.
      */
     @UnsupportedAppUsage
-    void freezeRotation(int rotation);
+    void freezeRotation(int rotation, String caller);
 
     /**
      * Equivalent to calling {@link #thawDisplayRotation(int)} with {@link
      * android.view.Display#DEFAULT_DISPLAY}.
      */
     @UnsupportedAppUsage
-    void thawRotation();
+    void thawRotation(String caller);
 
     /**
      * Equivelant to call {@link #isDisplayRotationFrozen(int)} with {@link
@@ -341,7 +341,7 @@
      *        {@link android.view.Surface#ROTATION_270} or -1 to freeze it to current rotation.
      * @hide
      */
-    void freezeDisplayRotation(int displayId, int rotation);
+    void freezeDisplayRotation(int displayId, int rotation, String caller);
 
     /**
      * Release the orientation lock imposed by freezeRotation() on the display.
@@ -349,7 +349,7 @@
      * @param displayId the ID of display which rotation should be thawed.
      * @hide
      */
-    void thawDisplayRotation(int displayId);
+    void thawDisplayRotation(int displayId, String caller);
 
     /**
      * Gets whether the rotation is frozen on the display.
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/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 0ba5d06..f421351 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -3858,9 +3858,7 @@
                 mPendingTransitions.clear();
             }
 
-            if (mActiveSurfaceSyncGroup != null) {
-                mActiveSurfaceSyncGroup.markSyncReady();
-            }
+            handleSyncRequestWhenNoAsyncDraw(mActiveSurfaceSyncGroup, mPendingTransaction);
         } else if (cancelAndRedraw) {
             mLastPerformTraversalsSkipDrawReason = cancelDueToPreDrawListener
                 ? "predraw_" + mAttachInfo.mTreeObserver.getLastDispatchOnPreDrawCanceledReason()
@@ -3874,8 +3872,8 @@
                 }
                 mPendingTransitions.clear();
             }
-            if (!performDraw(mActiveSurfaceSyncGroup) && mActiveSurfaceSyncGroup != null) {
-                mActiveSurfaceSyncGroup.markSyncReady();
+            if (!performDraw(mActiveSurfaceSyncGroup)) {
+                handleSyncRequestWhenNoAsyncDraw(mActiveSurfaceSyncGroup, mPendingTransaction);
             }
         }
 
@@ -3890,6 +3888,7 @@
             mReportNextDraw = false;
             mLastReportNextDrawReason = null;
             mActiveSurfaceSyncGroup = null;
+            mHasPendingTransactions = false;
             mSyncBuffer = false;
             if (isInWMSRequestedSync()) {
                 mWmsRequestSyncGroup.markSyncReady();
@@ -4688,7 +4687,8 @@
             return false;
         }
 
-        final boolean fullRedrawNeeded = mFullRedrawNeeded || surfaceSyncGroup != null;
+        final boolean fullRedrawNeeded =
+                mFullRedrawNeeded || surfaceSyncGroup != null || mHasPendingTransactions;
         mFullRedrawNeeded = false;
 
         mIsDrawing = true;
@@ -4718,8 +4718,15 @@
             mAttachInfo.mPendingAnimatingRenderNodes.clear();
         }
 
-        if (mReportNextDraw) {
+        final Transaction pendingTransaction;
+        if (!usingAsyncReport && mHasPendingTransactions) {
+            pendingTransaction = new Transaction();
+            pendingTransaction.merge(mPendingTransaction);
+        } else {
+            pendingTransaction = null;
+        }
 
+        if (mReportNextDraw) {
             // if we're using multi-thread renderer, wait for the window frame draws
             if (mWindowDrawCountDown != null) {
                 try {
@@ -4741,9 +4748,7 @@
             if (mSurfaceHolder != null && mSurface.isValid()) {
                 usingAsyncReport = true;
                 SurfaceCallbackHelper sch = new SurfaceCallbackHelper(() -> {
-                    if (surfaceSyncGroup != null) {
-                        surfaceSyncGroup.markSyncReady();
-                    }
+                    handleSyncRequestWhenNoAsyncDraw(surfaceSyncGroup, pendingTransaction);
                 });
 
                 SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();
@@ -4756,15 +4761,27 @@
             }
         }
 
-        if (surfaceSyncGroup != null && !usingAsyncReport) {
-            surfaceSyncGroup.markSyncReady();
+        if (!usingAsyncReport) {
+            handleSyncRequestWhenNoAsyncDraw(surfaceSyncGroup, pendingTransaction);
         }
+
         if (mPerformContentCapture) {
             performContentCaptureInitialReport();
         }
         return true;
     }
 
+    private void handleSyncRequestWhenNoAsyncDraw(SurfaceSyncGroup surfaceSyncGroup,
+            @Nullable Transaction pendingTransaction) {
+        if (surfaceSyncGroup != null) {
+            if (pendingTransaction != null) {
+                surfaceSyncGroup.addTransaction(pendingTransaction);
+            }
+            surfaceSyncGroup.markSyncReady();
+        } else if (pendingTransaction != null) {
+            pendingTransaction.apply();
+        }
+    }
     /**
      * Checks (and caches) if content capture is enabled for this context.
      */
@@ -4850,8 +4867,8 @@
         }
     }
 
-    private boolean draw(boolean fullRedrawNeeded,
-            @Nullable SurfaceSyncGroup activeSyncGroup, boolean syncBuffer) {
+    private boolean draw(boolean fullRedrawNeeded, @Nullable SurfaceSyncGroup activeSyncGroup,
+            boolean syncBuffer) {
         Surface surface = mSurface;
         if (!surface.isValid()) {
             return false;
@@ -4995,12 +5012,11 @@
                         mAttachInfo.mThreadedRenderer.forceDrawNextFrame();
                     }
                 } else if (mHasPendingTransactions) {
-                    // Register a calback if there's no sync involved but there were calls to
+                    // Register a callback if there's no sync involved but there were calls to
                     // applyTransactionOnDraw. If there is a sync involved, the sync callback will
                     // handle merging the pending transaction.
                     registerCallbackForPendingTransactions();
                 }
-                mHasPendingTransactions = false;
 
                 mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
             } else {
@@ -8977,13 +8993,7 @@
             mAdded = false;
             AnimationHandler.removeRequestor(this);
         }
-        if (mActiveSurfaceSyncGroup != null) {
-            mActiveSurfaceSyncGroup.markSyncReady();
-            mActiveSurfaceSyncGroup = null;
-        }
-        if (mHasPendingTransactions) {
-            mPendingTransaction.apply();
-        }
+        handleSyncRequestWhenNoAsyncDraw(mActiveSurfaceSyncGroup, mPendingTransaction);
         WindowManagerGlobal.getInstance().doRemoveView(this);
     }
 
@@ -11502,9 +11512,7 @@
             Log.d(mTag, "registerCallbacksForSync syncBuffer=" + syncBuffer);
         }
 
-        Transaction t = new Transaction();
-        t.merge(mPendingTransaction);
-
+        surfaceSyncGroup.addTransaction(mPendingTransaction);
         mAttachInfo.mThreadedRenderer.registerRtFrameCallback(new FrameDrawingCallback() {
             @Override
             public void onFrameDraw(long frame) {
@@ -11518,7 +11526,6 @@
                                     + frame + ".");
                 }
 
-                mergeWithNextTransaction(t, frame);
                 // If the syncResults are SYNC_LOST_SURFACE_REWARD_IF_FOUND or
                 // SYNC_CONTEXT_IS_STOPPED it means nothing will draw. There's no need to set up
                 // any blast sync or commit callback, and the code should directly call
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/TransitionRequestInfo.java b/core/java/android/window/TransitionRequestInfo.java
index 9b10a7f..932608a3 100644
--- a/core/java/android/window/TransitionRequestInfo.java
+++ b/core/java/android/window/TransitionRequestInfo.java
@@ -16,6 +16,8 @@
 
 package android.window;
 
+import static android.view.WindowManager.transitTypeToString;
+
 import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.WindowConfiguration;
@@ -88,6 +90,11 @@
         this(type, triggerTask, null /* pipTask */, remoteTransition, displayChange, flags);
     }
 
+    /** @hide */
+    String typeToString() {
+        return transitTypeToString(mType);
+    }
+
     /** Requested change to a display. */
     @DataClass(genToString = true, genSetters = true, genBuilder = false, genConstructor = false)
     public static class DisplayChange implements Parcelable {
@@ -263,7 +270,7 @@
         };
 
         @DataClass.Generated(
-                time = 1693425051905L,
+                time = 1695667226050L,
                 codegenVersion = "1.0.23",
                 sourceFile = "frameworks/base/core/java/android/window/TransitionRequestInfo.java",
                 inputSignatures = "private final  int mDisplayId\nprivate @android.annotation.Nullable android.graphics.Rect mStartAbsBounds\nprivate @android.annotation.Nullable android.graphics.Rect mEndAbsBounds\nprivate  int mStartRotation\nprivate  int mEndRotation\nprivate  boolean mPhysicalDisplayChanged\nclass DisplayChange extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genSetters=true, genBuilder=false, genConstructor=false)")
@@ -298,11 +305,11 @@
      * @param type
      *   The type of the transition being requested.
      * @param triggerTask
-     *   If non-null, If non-null, the task containing the activity whose lifecycle change (start or
+     *   If non-null, the task containing the activity whose lifecycle change (start or
      *   finish) has caused this transition to occur.
      * @param pipTask
-     *   If non-null, If non-null, the task containing the activity whose lifecycle change (start or
-     *   finish) has caused this transition to occur.
+     *   If non-null, the task containing the pip activity that participates in this
+     *   transition.
      * @param remoteTransition
      *   If non-null, a remote-transition associated with the source of this transition.
      * @param displayChange
@@ -431,7 +438,7 @@
         // String fieldNameToString() { ... }
 
         return "TransitionRequestInfo { " +
-                "type = " + mType + ", " +
+                "type = " + typeToString() + ", " +
                 "triggerTask = " + mTriggerTask + ", " +
                 "pipTask = " + mPipTask + ", " +
                 "remoteTransition = " + mRemoteTransition + ", " +
@@ -506,10 +513,10 @@
     };
 
     @DataClass.Generated(
-            time = 1693425051928L,
+            time = 1695667226088L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/core/java/android/window/TransitionRequestInfo.java",
-            inputSignatures = "private final @android.view.WindowManager.TransitionType int mType\nprivate @android.annotation.Nullable android.app.ActivityManager.RunningTaskInfo mTriggerTask\nprivate @android.annotation.Nullable android.app.ActivityManager.RunningTaskInfo mPipTask\nprivate @android.annotation.Nullable android.window.RemoteTransition mRemoteTransition\nprivate @android.annotation.Nullable android.window.TransitionRequestInfo.DisplayChange mDisplayChange\nprivate final  int mFlags\nclass TransitionRequestInfo extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genSetters=true, genAidl=true)")
+            inputSignatures = "private final @android.view.WindowManager.TransitionType int mType\nprivate @android.annotation.Nullable android.app.ActivityManager.RunningTaskInfo mTriggerTask\nprivate @android.annotation.Nullable android.app.ActivityManager.RunningTaskInfo mPipTask\nprivate @android.annotation.Nullable android.window.RemoteTransition mRemoteTransition\nprivate @android.annotation.Nullable android.window.TransitionRequestInfo.DisplayChange mDisplayChange\nprivate final  int mFlags\n  java.lang.String typeToString()\nclass TransitionRequestInfo extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genSetters=true, genAidl=true)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/core/java/com/android/internal/content/PackageMonitor.java b/core/java/com/android/internal/content/PackageMonitor.java
index 0d1871d..663067c 100644
--- a/core/java/com/android/internal/content/PackageMonitor.java
+++ b/core/java/com/android/internal/content/PackageMonitor.java
@@ -203,14 +203,22 @@
         }
         return false;
     }
-    
+
+    /**
+     * Direct reflection of {@link Intent#ACTION_PACKAGE_CHANGED
+     * Intent.ACTION_PACKAGE_CHANGED} being received, this callback
+     * has extras passed in.
+     */
+    public void onPackageChangedWithExtras(String packageName, Bundle extras) {
+    }
+
     public boolean onHandleForceStop(Intent intent, String[] packages, int uid, boolean doit) {
         return false;
     }
 
     public void onHandleUserStop(Intent intent, int userHandle) {
     }
-    
+
     public void onUidRemoved(int uid) {
     }
     
@@ -238,21 +246,34 @@
     }
 
     /**
+     * Called when a package disappears with extras passed in.
+     */
+    public void onPackageDisappearedWithExtras(String packageName, Bundle extras) {
+    }
+
+    /**
      * Called when a package appears for any reason.
      */
     public void onPackageAppeared(String packageName, int reason) {
     }
 
+
+    /**
+     * Called when a package appears with extras passed in.
+     */
+    public void onPackageAppearedWithExtras(String packageName, Bundle extras) {
+    }
+
     /**
      * Called when an existing package is updated or its disabled state changes.
      */
     public void onPackageModified(@NonNull String packageName) {
     }
-    
+
     public boolean didSomePackagesChange() {
         return mSomePackagesChanged;
     }
-    
+
     public int isPackageAppearing(String packageName) {
         if (mAppearingPackages != null) {
             for (int i=mAppearingPackages.length-1; i>=0; i--) {
@@ -381,6 +402,7 @@
                     mChangeType = PACKAGE_PERMANENT_CHANGE;
                     onPackageAdded(pkg, uid);
                 }
+                onPackageAppearedWithExtras(pkg, intent.getExtras());
                 onPackageAppeared(pkg, mChangeType);
             }
         } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
@@ -403,6 +425,7 @@
                         onPackageRemovedAllUsers(pkg, uid);
                     }
                 }
+                onPackageDisappearedWithExtras(pkg, intent.getExtras());
                 onPackageDisappeared(pkg, mChangeType);
             }
         } else if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
@@ -417,6 +440,7 @@
                 if (onPackageChanged(pkg, uid, mModifiedComponents)) {
                     mSomePackagesChanged = true;
                 }
+                onPackageChangedWithExtras(pkg, intent.getExtras());
                 onPackageModified(pkg);
             }
         } else if (Intent.ACTION_PACKAGE_DATA_CLEARED.equals(action)) {
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/view/RotationPolicy.java b/core/java/com/android/internal/view/RotationPolicy.java
index 058c6ec..6e45796 100644
--- a/core/java/com/android/internal/view/RotationPolicy.java
+++ b/core/java/com/android/internal/view/RotationPolicy.java
@@ -105,23 +105,23 @@
     /**
      * Enables or disables rotation lock from the system UI toggle.
      */
-    public static void setRotationLock(Context context, final boolean enabled) {
+    public static void setRotationLock(Context context, final boolean enabled, String caller) {
         final int rotation = areAllRotationsAllowed(context)
                 || useCurrentRotationOnRotationLockChange(context) ? CURRENT_ROTATION
                 : NATURAL_ROTATION;
-        setRotationLockAtAngle(context, enabled, rotation);
+        setRotationLockAtAngle(context, enabled, rotation, caller);
     }
 
     /**
      * Enables or disables rotation lock at a specific rotation from system UI.
      */
     public static void setRotationLockAtAngle(Context context, final boolean enabled,
-            final int rotation) {
+            final int rotation, String caller) {
         Settings.System.putIntForUser(context.getContentResolver(),
                 Settings.System.HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY, 0,
                 UserHandle.USER_CURRENT);
 
-        setRotationLock(enabled, rotation);
+        setRotationLock(enabled, rotation, caller);
     }
 
     /**
@@ -129,12 +129,13 @@
      *
      * If rotation is locked for accessibility, the system UI toggle is hidden to avoid confusion.
      */
-    public static void setRotationLockForAccessibility(Context context, final boolean enabled) {
+    public static void setRotationLockForAccessibility(Context context, final boolean enabled,
+            String caller) {
         Settings.System.putIntForUser(context.getContentResolver(),
                 Settings.System.HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY, enabled ? 1 : 0,
                         UserHandle.USER_CURRENT);
 
-        setRotationLock(enabled, NATURAL_ROTATION);
+        setRotationLock(enabled, NATURAL_ROTATION, caller);
     }
 
     private static boolean areAllRotationsAllowed(Context context) {
@@ -146,16 +147,17 @@
                 R.bool.config_useCurrentRotationOnRotationLockChange);
     }
 
-    private static void setRotationLock(final boolean enabled, final int rotation) {
+    private static void setRotationLock(final boolean enabled, final int rotation,
+            final String caller) {
         AsyncTask.execute(new Runnable() {
             @Override
             public void run() {
                 try {
                     IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
                     if (enabled) {
-                        wm.freezeRotation(rotation);
+                        wm.freezeRotation(rotation, caller);
                     } else {
-                        wm.thawRotation();
+                        wm.thawRotation(caller);
                     }
                 } catch (RemoteException exc) {
                     Log.w(TAG, "Unable to save auto-rotate setting");
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 ad196c0..3795fc8 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -15,7 +15,19 @@
     ],
 }
 
-cc_library_shared {
+soong_config_module_type {
+    name: "cc_library_shared_for_libandroid_runtime",
+    module_type: "cc_library_shared",
+    config_namespace: "ANDROID",
+    bool_variables: [
+        "release_binder_death_recipient_weak_from_jni",
+    ],
+    properties: [
+        "cflags",
+    ],
+}
+
+cc_library_shared_for_libandroid_runtime {
     name: "libandroid_runtime",
     host_supported: true,
     cflags: [
@@ -46,6 +58,12 @@
         },
     },
 
+    soong_config_variables: {
+        release_binder_death_recipient_weak_from_jni: {
+            cflags: ["-DBINDER_DEATH_RECIPIENT_WEAK_FROM_JNI"],
+        },
+    },
+
     cpp_std: "gnu++20",
 
     srcs: [
@@ -84,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/core/jni/android_util_Binder.cpp b/core/jni/android_util_Binder.cpp
index 041f9c7..bfd80a9e 100644
--- a/core/jni/android_util_Binder.cpp
+++ b/core/jni/android_util_Binder.cpp
@@ -17,19 +17,8 @@
 #define LOG_TAG "JavaBinder"
 //#define LOG_NDEBUG 0
 
-#include "android_os_Parcel.h"
 #include "android_util_Binder.h"
 
-#include <atomic>
-#include <fcntl.h>
-#include <inttypes.h>
-#include <mutex>
-#include <stdio.h>
-#include <string>
-#include <sys/stat.h>
-#include <sys/types.h>
-#include <unistd.h>
-
 #include <android-base/stringprintf.h>
 #include <binder/BpBinder.h>
 #include <binder/IInterface.h>
@@ -40,7 +29,16 @@
 #include <binder/Stability.h>
 #include <binderthreadstate/CallerUtils.h>
 #include <cutils/atomic.h>
+#include <fcntl.h>
+#include <inttypes.h>
 #include <log/log.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedLocalRef.h>
+#include <nativehelper/ScopedUtfChars.h>
+#include <stdio.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
 #include <utils/KeyedVector.h>
 #include <utils/List.h>
 #include <utils/Log.h>
@@ -48,10 +46,11 @@
 #include <utils/SystemClock.h>
 #include <utils/threads.h>
 
-#include <nativehelper/JNIHelp.h>
-#include <nativehelper/ScopedLocalRef.h>
-#include <nativehelper/ScopedUtfChars.h>
+#include <atomic>
+#include <mutex>
+#include <string>
 
+#include "android_os_Parcel.h"
 #include "core_jni_helpers.h"
 
 //#undef ALOGV
@@ -553,14 +552,48 @@
 };
 
 // ----------------------------------------------------------------------------
+#ifdef BINDER_DEATH_RECIPIENT_WEAK_FROM_JNI
+#if __BIONIC__
+#include <android/api-level.h>
+static bool target_sdk_is_at_least_vic() {
+    return android_get_application_target_sdk_version() >= __ANDROID_API_V__;
+}
+#else
+static constexpr bool target_sdk_is_at_least_vic() {
+    // If not built for Android (i.e. glibc host), follow the latest behavior as there's no compat
+    // requirement there.
+    return true;
+}
+#endif // __BIONIC__
+#endif // BINDER_DEATH_RECIPIENT_WEAK_FROM_JNI
 
 class JavaDeathRecipient : public IBinder::DeathRecipient
 {
 public:
     JavaDeathRecipient(JNIEnv* env, jobject object, const sp<DeathRecipientList>& list)
-        : mVM(jnienv_to_javavm(env)), mObject(env->NewGlobalRef(object)),
-          mObjectWeak(NULL), mList(list)
-    {
+          : mVM(jnienv_to_javavm(env)), mObject(NULL), mObjectWeak(NULL), mList(list) {
+        // b/298374304: For apps targeting Android V or beyond, we no longer hold the global JNI ref
+        // to the death recipient objects. This is to prevent the memory leak which can happen when
+        // the death recipient object internally has a strong reference to the proxy object. Under
+        // the old behavior, you were unable to kill the binder service by dropping all references
+        // to the proxy object - because it is still strong referenced from JNI (here). The only way
+        // to cut the strong reference was to call unlinkDeath(), but it was easy to forget.
+        //
+        // Now, the strong reference to the death recipient is held in the Java-side proxy object.
+        // See BinderProxy.mDeathRecipients. From JNI, only the weak reference is kept. An
+        // implication of this is that you may not receive binderDied() if you drop all references
+        // to the proxy object before the service dies. This should be okay for most cases because
+        // you normally are not interested in the death of a binder service which you don't have any
+        // reference to. If however you want to get binderDied() regardless of the proxy object's
+        // lifecycle, keep a strong reference to the death recipient object by yourself.
+#ifdef BINDER_DEATH_RECIPIENT_WEAK_FROM_JNI
+        if (target_sdk_is_at_least_vic()) {
+            mObjectWeak = env->NewWeakGlobalRef(object);
+        } else
+#endif
+        {
+            mObject = env->NewGlobalRef(object);
+        }
         // These objects manage their own lifetimes so are responsible for final bookkeeping.
         // The list holds a strong reference to this object.
         LOGDEATH("Adding JDR %p to DRL %p", this, list.get());
@@ -573,26 +606,49 @@
     void binderDied(const wp<IBinder>& who)
     {
         LOGDEATH("Receiving binderDied() on JavaDeathRecipient %p\n", this);
-        if (mObject != NULL) {
-            JNIEnv* env = javavm_to_jnienv(mVM);
-            ScopedLocalRef<jobject> jBinderProxy(env, javaObjectForIBinder(env, who.promote()));
-            env->CallStaticVoidMethod(gBinderProxyOffsets.mClass,
-                                      gBinderProxyOffsets.mSendDeathNotice, mObject,
-                                      jBinderProxy.get());
-            if (env->ExceptionCheck()) {
-                jthrowable excep = env->ExceptionOccurred();
-                binder_report_exception(env, excep,
-                                        "*** Uncaught exception returned from death notification!");
-            }
+        if (mObject == NULL && mObjectWeak == NULL) {
+            return;
+        }
+        JNIEnv* env = javavm_to_jnienv(mVM);
+        ScopedLocalRef<jobject> jBinderProxy(env, javaObjectForIBinder(env, who.promote()));
 
-            // Serialize with our containing DeathRecipientList so that we can't
-            // delete the global ref on mObject while the list is being iterated.
+        // Hold a local reference to the recipient. This may fail if the recipient is weakly
+        // referenced, in which case we can't deliver the death notice.
+        ScopedLocalRef<jobject> jRecipient(env,
+                                           env->NewLocalRef(mObject != NULL ? mObject
+                                                                            : mObjectWeak));
+        if (jRecipient.get() == NULL) {
+            ALOGW("Binder died, but death recipient is already garbage collected. If your target "
+                  "sdk level is at or above 35, this can happen when you dropped all references to "
+                  "the binder service before it died. If you want to get a death notice for a "
+                  "binder service which you have no reference to, keep a strong reference to the "
+                  "death recipient by yourself.");
+            return;
+        }
+
+        if (mFired) {
+            ALOGW("Received multiple death notices for the same binder object. Binder driver bug?");
+            return;
+        }
+        mFired = true;
+
+        env->CallStaticVoidMethod(gBinderProxyOffsets.mClass, gBinderProxyOffsets.mSendDeathNotice,
+                                  jRecipient.get(), jBinderProxy.get());
+        if (env->ExceptionCheck()) {
+            jthrowable excep = env->ExceptionOccurred();
+            binder_report_exception(env, excep,
+                                    "*** Uncaught exception returned from death notification!");
+        }
+
+        // Demote from strong ref (if exists) to weak after binderDied() has been delivered, to
+        // allow the DeathRecipient and BinderProxy to be GC'd if no longer needed. Do this in sync
+        // with our containing DeathRecipientList so that we can't delete the global ref on mObject
+        // while the list is being iterated.
+        if (mObject != NULL) {
             sp<DeathRecipientList> list = mList.promote();
             if (list != NULL) {
                 AutoMutex _l(list->lock());
 
-                // Demote from strong ref to weak after binderDied() has been delivered,
-                // to allow the DeathRecipient and BinderProxy to be GC'd if no longer needed.
                 mObjectWeak = env->NewWeakGlobalRef(mObject);
                 env->DeleteGlobalRef(mObject);
                 mObject = NULL;
@@ -659,9 +715,19 @@
 
 private:
     JavaVM* const mVM;
-    jobject mObject;  // Initial strong ref to Java-side DeathRecipient. Cleared on binderDied().
-    jweak mObjectWeak; // Weak ref to the same Java-side DeathRecipient after binderDied().
+
+    // If target sdk version < 35, the Java-side DeathRecipient is strongly referenced from mObject
+    // upon linkToDeath() and then after binderDied() is called, the strong reference is demoted to
+    // a weak reference (mObjectWeak).
+    // If target sdk version >= 35, the strong reference is never made here (i.e. mObject == NULL
+    // always). Instead, the strong reference to the Java-side DeathRecipient is made in
+    // BinderProxy.mDeathRecipients. In the native world, only the weak reference is kept.
+    jobject mObject;
+    jweak mObjectWeak;
     wp<DeathRecipientList> mList;
+
+    // Whether binderDied was called or not.
+    bool mFired = false;
 };
 
 // ----------------------------------------------------------------------------
@@ -1435,17 +1501,19 @@
 
 // ----------------------------------------------------------------------------
 
+// clang-format off
 static const JNINativeMethod gBinderProxyMethods[] = {
      /* name, signature, funcPtr */
     {"pingBinder",          "()Z", (void*)android_os_BinderProxy_pingBinder},
     {"isBinderAlive",       "()Z", (void*)android_os_BinderProxy_isBinderAlive},
     {"getInterfaceDescriptor", "()Ljava/lang/String;", (void*)android_os_BinderProxy_getInterfaceDescriptor},
     {"transactNative",      "(ILandroid/os/Parcel;Landroid/os/Parcel;I)Z", (void*)android_os_BinderProxy_transact},
-    {"linkToDeath",         "(Landroid/os/IBinder$DeathRecipient;I)V", (void*)android_os_BinderProxy_linkToDeath},
-    {"unlinkToDeath",       "(Landroid/os/IBinder$DeathRecipient;I)Z", (void*)android_os_BinderProxy_unlinkToDeath},
+    {"linkToDeathNative",   "(Landroid/os/IBinder$DeathRecipient;I)V", (void*)android_os_BinderProxy_linkToDeath},
+    {"unlinkToDeathNative", "(Landroid/os/IBinder$DeathRecipient;I)Z", (void*)android_os_BinderProxy_unlinkToDeath},
     {"getNativeFinalizer",  "()J", (void*)android_os_BinderProxy_getNativeFinalizer},
     {"getExtension",        "()Landroid/os/IBinder;", (void*)android_os_BinderProxy_getExtension},
 };
+// clang-format on
 
 const char* const kBinderProxyPathName = "android/os/BinderProxy";
 
diff --git a/core/jni/android_view_DisplayEventReceiver.cpp b/core/jni/android_view_DisplayEventReceiver.cpp
index 69fc515..41c65ae 100644
--- a/core/jni/android_view_DisplayEventReceiver.cpp
+++ b/core/jni/android_view_DisplayEventReceiver.cpp
@@ -38,6 +38,7 @@
 
     jmethodID dispatchVsync;
     jmethodID dispatchHotplug;
+    jmethodID dispatchHotplugConnectionError;
     jmethodID dispatchModeChanged;
     jmethodID dispatchFrameRateOverrides;
 
@@ -89,6 +90,7 @@
     void dispatchVsync(nsecs_t timestamp, PhysicalDisplayId displayId, uint32_t count,
                        VsyncEventData vsyncEventData) override;
     void dispatchHotplug(nsecs_t timestamp, PhysicalDisplayId displayId, bool connected) override;
+    void dispatchHotplugConnectionError(nsecs_t timestamp, int errorCode) override;
     void dispatchModeChanged(nsecs_t timestamp, PhysicalDisplayId displayId, int32_t modeId,
                              nsecs_t renderPeriod) override;
     void dispatchFrameRateOverrides(nsecs_t timestamp, PhysicalDisplayId displayId,
@@ -230,6 +232,22 @@
     mMessageQueue->raiseAndClearException(env, "dispatchHotplug");
 }
 
+void NativeDisplayEventReceiver::dispatchHotplugConnectionError(nsecs_t timestamp,
+                                                                int connectionError) {
+    JNIEnv* env = AndroidRuntime::getJNIEnv();
+
+    ScopedLocalRef<jobject> receiverObj(env, GetReferent(env, mReceiverWeakGlobal));
+    if (receiverObj.get()) {
+        ALOGV("receiver %p ~ Invoking hotplug dispatchHotplugConnectionError handler.", this);
+        env->CallVoidMethod(receiverObj.get(),
+                            gDisplayEventReceiverClassInfo.dispatchHotplugConnectionError,
+                            timestamp, connectionError);
+        ALOGV("receiver %p ~ Returned from hotplug dispatchHotplugConnectionError handler.", this);
+    }
+
+    mMessageQueue->raiseAndClearException(env, "dispatchHotplugConnectionError");
+}
+
 void NativeDisplayEventReceiver::dispatchModeChanged(nsecs_t timestamp, PhysicalDisplayId displayId,
                                                      int32_t modeId, nsecs_t renderPeriod) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
@@ -354,8 +372,12 @@
 
     gDisplayEventReceiverClassInfo.dispatchVsync =
             GetMethodIDOrDie(env, gDisplayEventReceiverClassInfo.clazz, "dispatchVsync", "(JJI)V");
-    gDisplayEventReceiverClassInfo.dispatchHotplug = GetMethodIDOrDie(env,
-            gDisplayEventReceiverClassInfo.clazz, "dispatchHotplug", "(JJZ)V");
+    gDisplayEventReceiverClassInfo.dispatchHotplug =
+            GetMethodIDOrDie(env, gDisplayEventReceiverClassInfo.clazz, "dispatchHotplug",
+                             "(JJZ)V");
+    gDisplayEventReceiverClassInfo.dispatchHotplugConnectionError =
+            GetMethodIDOrDie(env, gDisplayEventReceiverClassInfo.clazz,
+                             "dispatchHotplugConnectionError", "(JI)V");
     gDisplayEventReceiverClassInfo.dispatchModeChanged =
             GetMethodIDOrDie(env, gDisplayEventReceiverClassInfo.clazz, "dispatchModeChanged",
                              "(JJIJ)V");
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 3d0af3d..c00a776f 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -5227,6 +5227,28 @@
          non-zero. -->
     <integer name="config_defaultPeakRefreshRate">0</integer>
 
+    <!-- External display peak refresh rate for the given device. Change this value if you want to
+         prevent the framework from using higher refresh rates, even if display modes with higher
+         refresh rates are available from hardware composer. Only has an effect if this value and
+         config_externalDisplayPeakWidth and config_externalDisplayPeakHeight are non-zero. -->
+    <integer name="config_externalDisplayPeakRefreshRate">0</integer>
+
+    <!-- External display peak width for the given device. Change this value if you want
+         to prevent the framework from using higher resolution, even if display modes with higher
+         resolutions are available from hardware composer. Only has an effect if this value and
+         config_externalDisplayPeakRefreshRate and config_externalDisplayPeakHeight are non-zero.-->
+    <integer name="config_externalDisplayPeakWidth">0</integer>
+
+    <!-- External display peak height for the given device. Change this value if you want
+         to prevent the framework from using higher resolution, even if display modes with higher
+         resolutions are available from hardware composer. Only has an effect if this value and
+         config_externalDisplayPeakRefreshRate and config_externalDisplayPeakWidth are non-zero. -->
+    <integer name="config_externalDisplayPeakHeight">0</integer>
+
+    <!-- Enable synchronization of the displays refresh rates by applying the default low refresh
+         rate. -->
+    <bool name="config_refreshRateSynchronizationEnabled">false</bool>
+
     <!-- The display uses different gamma curves for different refresh rates. It's hard for panel
          vendors to tune the curves to have exact same brightness for different refresh rate. So
          flicker could be observed at switch time. The issue is worse at the gamma lower end.
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 83fb098..b0eee1c 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4231,6 +4231,10 @@
   <!-- For high refresh rate displays -->
   <java-symbol type="integer" name="config_defaultRefreshRate" />
   <java-symbol type="integer" name="config_defaultPeakRefreshRate" />
+  <java-symbol type="integer" name="config_externalDisplayPeakRefreshRate" />
+  <java-symbol type="integer" name="config_externalDisplayPeakWidth" />
+  <java-symbol type="integer" name="config_externalDisplayPeakHeight" />
+  <java-symbol type="bool" name="config_refreshRateSynchronizationEnabled" />
   <java-symbol type="integer" name="config_defaultRefreshRateInZone" />
   <java-symbol type="array" name="config_brightnessThresholdsOfPeakRefreshRate" />
   <java-symbol type="array" name="config_ambientThresholdsOfPeakRefreshRate" />
diff --git a/core/tests/packagemonitortests/src/com/android/internal/content/PackageMonitorTest.java b/core/tests/packagemonitortests/src/com/android/internal/content/PackageMonitorTest.java
index e082c25..c7eddabe 100644
--- a/core/tests/packagemonitortests/src/com/android/internal/content/PackageMonitorTest.java
+++ b/core/tests/packagemonitortests/src/com/android/internal/content/PackageMonitorTest.java
@@ -17,7 +17,6 @@
 package com.android.internal.content;
 
 import static com.google.common.truth.Truth.assertThat;
-
 import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
@@ -28,6 +27,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.UserHandle;
 
@@ -36,6 +36,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -45,6 +46,7 @@
 @RunWith(AndroidJUnit4.class)
 public class PackageMonitorTest {
     private static final String FAKE_PACKAGE_NAME = "com.android.internal.content.fakeapp";
+    private static final String FAKE_EXTRA_REASON = "android.intent.extra.fakereason";
     private static final int FAKE_PACKAGE_UID = 123;
     private static final int FAKE_USER_ID = 0;
     private static final int WAIT_CALLBACK_CALLED_IN_MS = 300;
@@ -245,6 +247,7 @@
         intent.setData(Uri.fromParts("package", FAKE_PACKAGE_NAME, null));
         intent.putExtra(Intent.EXTRA_USER_HANDLE, FAKE_USER_ID);
         intent.putExtra(Intent.EXTRA_UID, FAKE_PACKAGE_UID);
+        intent.putExtra(Intent.EXTRA_REASON, FAKE_EXTRA_REASON);
         String [] packageList = new String[]{FAKE_PACKAGE_NAME};
         intent.putExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, packageList);
         spyPackageMonitor.doHandlePackageEvent(intent);
@@ -253,6 +256,20 @@
         verify(spyPackageMonitor, times(1))
                 .onPackageChanged(eq(FAKE_PACKAGE_NAME), eq(FAKE_PACKAGE_UID), eq(packageList));
         verify(spyPackageMonitor, times(1)).onPackageModified(eq(FAKE_PACKAGE_NAME));
+
+        ArgumentCaptor<Bundle> argumentCaptor = ArgumentCaptor.forClass(Bundle.class);
+        verify(spyPackageMonitor, times(1)).onPackageChangedWithExtras(eq(FAKE_PACKAGE_NAME),
+                argumentCaptor.capture());
+
+        Bundle capturedExtras = argumentCaptor.getValue();
+        Bundle expectedExtras = intent.getExtras();
+        assertThat(capturedExtras.getInt(Intent.EXTRA_USER_HANDLE))
+                .isEqualTo(expectedExtras.getInt(Intent.EXTRA_USER_HANDLE));
+        assertThat(capturedExtras.getInt(Intent.EXTRA_UID))
+                .isEqualTo(expectedExtras.getInt(Intent.EXTRA_UID));
+        assertThat(capturedExtras.getInt(Intent.EXTRA_REASON))
+                .isEqualTo(expectedExtras.getInt(Intent.EXTRA_REASON));
+
         verify(spyPackageMonitor, times(1)).onSomePackagesChanged();
         verify(spyPackageMonitor, times(1)).onFinishPackageChanges();
     }
@@ -272,6 +289,21 @@
         verify(spyPackageMonitor, times(1)).onBeginPackageChanges();
         verify(spyPackageMonitor, times(1))
                 .onPackageUpdateStarted(eq(FAKE_PACKAGE_NAME), eq(FAKE_PACKAGE_UID));
+
+        ArgumentCaptor<Bundle> argumentCaptor = ArgumentCaptor.forClass(Bundle.class);
+        verify(spyPackageMonitor, times(1)).onPackageDisappearedWithExtras(eq(FAKE_PACKAGE_NAME),
+                argumentCaptor.capture());
+        Bundle capturedExtras = argumentCaptor.getValue();
+        Bundle expectedExtras = intent.getExtras();
+        assertThat(capturedExtras.getInt(Intent.EXTRA_USER_HANDLE))
+                .isEqualTo(expectedExtras.getInt(Intent.EXTRA_USER_HANDLE));
+        assertThat(capturedExtras.getInt(Intent.EXTRA_UID))
+                .isEqualTo(expectedExtras.getInt(Intent.EXTRA_UID));
+        assertThat(capturedExtras.getInt(Intent.EXTRA_REPLACING))
+                .isEqualTo(expectedExtras.getInt(Intent.EXTRA_REPLACING));
+        assertThat(capturedExtras.getInt(Intent.EXTRA_REMOVED_FOR_ALL_USERS))
+                .isEqualTo(expectedExtras.getInt(Intent.EXTRA_REMOVED_FOR_ALL_USERS));
+
         verify(spyPackageMonitor, times(1))
                 .onPackageDisappeared(eq(FAKE_PACKAGE_NAME), eq(PackageMonitor.PACKAGE_UPDATING));
         verify(spyPackageMonitor, times(1)).onFinishPackageChanges();
@@ -295,6 +327,21 @@
                 .onPackageRemoved(eq(FAKE_PACKAGE_NAME), eq(FAKE_PACKAGE_UID));
         verify(spyPackageMonitor, times(1))
                 .onPackageRemovedAllUsers(eq(FAKE_PACKAGE_NAME), eq(FAKE_PACKAGE_UID));
+
+        ArgumentCaptor<Bundle> argumentCaptor = ArgumentCaptor.forClass(Bundle.class);
+        verify(spyPackageMonitor, times(1)).onPackageDisappearedWithExtras(eq(FAKE_PACKAGE_NAME),
+                argumentCaptor.capture());
+        Bundle capturedExtras = argumentCaptor.getValue();
+        Bundle expectedExtras = intent.getExtras();
+        assertThat(capturedExtras.getInt(Intent.EXTRA_USER_HANDLE))
+                .isEqualTo(expectedExtras.getInt(Intent.EXTRA_USER_HANDLE));
+        assertThat(capturedExtras.getInt(Intent.EXTRA_UID))
+                .isEqualTo(expectedExtras.getInt(Intent.EXTRA_UID));
+        assertThat(capturedExtras.getInt(Intent.EXTRA_REPLACING))
+                .isEqualTo(expectedExtras.getInt(Intent.EXTRA_REPLACING));
+        assertThat(capturedExtras.getInt(Intent.EXTRA_REMOVED_FOR_ALL_USERS))
+                .isEqualTo(expectedExtras.getInt(Intent.EXTRA_REMOVED_FOR_ALL_USERS));
+
         verify(spyPackageMonitor, times(1)).onPackageDisappeared(eq(FAKE_PACKAGE_NAME),
                 eq(PackageMonitor.PACKAGE_PERMANENT_CHANGE));
         verify(spyPackageMonitor, times(1)).onSomePackagesChanged();
@@ -316,6 +363,19 @@
         verify(spyPackageMonitor, times(1))
                 .onPackageUpdateFinished(eq(FAKE_PACKAGE_NAME), eq(FAKE_PACKAGE_UID));
         verify(spyPackageMonitor, times(1)).onPackageModified(eq(FAKE_PACKAGE_NAME));
+
+        ArgumentCaptor<Bundle> argumentCaptor = ArgumentCaptor.forClass(Bundle.class);
+        verify(spyPackageMonitor, times(1)).onPackageAppearedWithExtras(eq(FAKE_PACKAGE_NAME),
+                argumentCaptor.capture());
+        Bundle capturedExtras = argumentCaptor.getValue();
+        Bundle expectedExtras = intent.getExtras();
+        assertThat(capturedExtras.getInt(Intent.EXTRA_USER_HANDLE))
+                .isEqualTo(expectedExtras.getInt(Intent.EXTRA_USER_HANDLE));
+        assertThat(capturedExtras.getInt(Intent.EXTRA_UID))
+                .isEqualTo(expectedExtras.getInt(Intent.EXTRA_UID));
+        assertThat(capturedExtras.getInt(Intent.EXTRA_REPLACING))
+                .isEqualTo(expectedExtras.getInt(Intent.EXTRA_REPLACING));
+
         verify(spyPackageMonitor, times(1))
                 .onPackageAppeared(eq(FAKE_PACKAGE_NAME), eq(PackageMonitor.PACKAGE_UPDATING));
         verify(spyPackageMonitor, times(1)).onSomePackagesChanged();
@@ -336,6 +396,19 @@
         verify(spyPackageMonitor, times(1)).onBeginPackageChanges();
         verify(spyPackageMonitor, times(1))
                 .onPackageAdded(eq(FAKE_PACKAGE_NAME), eq(FAKE_PACKAGE_UID));
+
+        ArgumentCaptor<Bundle> argumentCaptor = ArgumentCaptor.forClass(Bundle.class);
+        verify(spyPackageMonitor, times(1)).onPackageAppearedWithExtras(eq(FAKE_PACKAGE_NAME),
+                argumentCaptor.capture());
+        Bundle capturedExtras = argumentCaptor.getValue();
+        Bundle expectedExtras = intent.getExtras();
+        assertThat(capturedExtras.getInt(Intent.EXTRA_USER_HANDLE))
+                .isEqualTo(expectedExtras.getInt(Intent.EXTRA_USER_HANDLE));
+        assertThat(capturedExtras.getInt(Intent.EXTRA_UID))
+                .isEqualTo(expectedExtras.getInt(Intent.EXTRA_UID));
+        assertThat(capturedExtras.getInt(Intent.EXTRA_REPLACING))
+                .isEqualTo(expectedExtras.getInt(Intent.EXTRA_REPLACING));
+
         verify(spyPackageMonitor, times(1)).onPackageAppeared(eq(FAKE_PACKAGE_NAME),
                 eq(PackageMonitor.PACKAGE_PERMANENT_CHANGE));
         verify(spyPackageMonitor, times(1)).onSomePackagesChanged();
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index ad0ead7..9c65287 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -1123,12 +1123,6 @@
       "group": "WM_SHOW_TRANSACTIONS",
       "at": "com\/android\/server\/wm\/WindowSurfaceController.java"
     },
-    "-1076978367": {
-      "message": "thawRotation: mRotation=%d",
-      "level": "VERBOSE",
-      "group": "WM_DEBUG_ORIENTATION",
-      "at": "com\/android\/server\/wm\/WindowManagerService.java"
-    },
     "-1075136930": {
       "message": "startLockTaskMode: Can't lock due to auth",
       "level": "WARN",
@@ -1231,6 +1225,12 @@
       "group": "WM_DEBUG_STARTING_WINDOW",
       "at": "com\/android\/server\/wm\/WindowState.java"
     },
+    "-962760979": {
+      "message": "thawRotation: mRotation=%d, caller=%s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_ORIENTATION",
+      "at": "com\/android\/server\/wm\/WindowManagerService.java"
+    },
     "-961053385": {
       "message": "attachWindowContextToDisplayArea: calling from non-existing process pid=%d uid=%d",
       "level": "WARN",
@@ -2779,6 +2779,12 @@
       "group": "WM_DEBUG_ORIENTATION",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "364992694": {
+      "message": "freezeDisplayRotation: current rotation=%d, new rotation=%d, caller=%s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_ORIENTATION",
+      "at": "com\/android\/server\/wm\/WindowManagerService.java"
+    },
     "371173718": {
       "message": "finishSync cancel=%b for %s",
       "level": "VERBOSE",
diff --git a/graphics/java/android/graphics/RuntimeShader.java b/graphics/java/android/graphics/RuntimeShader.java
index 3e64579..78d257f 100644
--- a/graphics/java/android/graphics/RuntimeShader.java
+++ b/graphics/java/android/graphics/RuntimeShader.java
@@ -49,11 +49,11 @@
  *  possible antialiasing logic for border pixels).</li>
  *  <li>Logic for the {@link Shader}, {@link ColorFilter}, and {@link BlendMode} on the
  *  {@link Paint}.</li>
- *  <li>Color space conversion code, as part of Android’s color management.</li>
+ *  <li>Color space conversion code, as part of Android's color management.</li>
  * </ul>
  *
  * <p>A {@link RuntimeShader}, like other {@link Shader} types, effectively contributes a function
- * to the GPU’s fragment shader.</p>
+ * to the GPU's fragment shader.</p>
  *
  * <h3>AGSL Shader Execution</h3>
  * <p>Just like a GLSL shader, an AGSL shader begins execution in a main function. Unlike GLSL, the
@@ -78,10 +78,10 @@
  * {@link ColorSpace} for an AGSL shader is defined to be the color space of the destination, which
  * in most cases is determined by {@link Window#setColorMode(int)}.</p>
  *
- * <p>When authoring an AGSL shader, you won’t know what the working color space is. For many
+ * <p>When authoring an AGSL shader, you won't know what the working color space is. For many
  * effects, this is fine because by default color inputs are automatically converted into the
  * working color space. For certain effects, it may be important to do some math in a fixed, known
- * color space. A common example is lighting – to get physically accurate lighting, math should be
+ * color space. A common example is lighting - to get physically accurate lighting, math should be
  * done in a linear color space. To help with this, AGSL provides two intrinsic functions that
  * convert colors between the working color space and the
  * {@link ColorSpace.Named#LINEAR_EXTENDED_SRGB} color space:
@@ -93,7 +93,7 @@
  * <h3>AGSL and Premultiplied Alpha</h3>
  * <p>When dealing with transparent colors, there are two (common) possible representations:
  * straight (unassociated) alpha and premultiplied (associated) alpha. In ASGL the color returned
- * by the main function is expected to be premultiplied.  AGSL’s use of premultiplied alpha
+ * by the main function is expected to be premultiplied.  AGSL's use of premultiplied alpha
  * implies:
  * </p>
  *
@@ -101,7 +101,7 @@
  *  <li>If your AGSL shader will return transparent colors, be sure to multiply the RGB by A.  The
  *  resulting color should be [R*A, G*A, B*A, A], not [R, G, B, A].</li>
  *  <li>For more complex shaders, you must understand which of your colors are premultiplied vs.
- *  straight. Many operations don’t make sense if you mix both kinds of color together.</li>
+ *  straight. Many operations don't make sense if you mix both kinds of color together.</li>
  * </ul>
  *
  * <h3>Uniforms</h3>
@@ -224,7 +224,7 @@
  * shader uniform is undefined if it is declared in the AGSL shader but not initialized.</p>
  *
  * <p>Although most {@link BitmapShader}s contain colors that should be color managed, some contain
- * data that isn’t actually colors. This includes bitmaps storing normals, material properties
+ * data that isn't actually colors. This includes bitmaps storing normals, material properties
  * (e.g. roughness), heightmaps, or any other purely mathematical data that happens to be stored in
  * a bitmap. When using these kinds of shaders in AGSL, you probably want to initialize them with
  * {@link #setInputBuffer(String, BitmapShader)}. Shaders initialized this way work much like
@@ -237,7 +237,7 @@
  *
  * <p>In addition, when sampling from a {@link BitmapShader} be aware that the shader does not use
  * normalized coordinates (like a texture in GLSL). It uses (0, 0) in the upper-left corner, and
- * (width, height) in the bottom-right corner. Normally, this is exactly what you want. If you’re
+ * (width, height) in the bottom-right corner. Normally, this is exactly what you want. If you're
  * evaluating the shader with coordinates based on the ones passed to your AGSL program, the scale
  * is correct. However, if you want to adjust those coordinates (to do some kind of re-mapping of
  * the bitmap), remember that the coordinates are local to the canvas.</p>
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/graphics/java/android/graphics/fonts/FontFamily.java b/graphics/java/android/graphics/fonts/FontFamily.java
index 7cca7f1..5e41105 100644
--- a/graphics/java/android/graphics/fonts/FontFamily.java
+++ b/graphics/java/android/graphics/fonts/FontFamily.java
@@ -16,6 +16,9 @@
 
 package android.graphics.fonts;
 
+import static com.android.text.flags.Flags.FLAG_DEPRECATE_FONTS_XML;
+
+import android.annotation.FlaggedApi;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -138,6 +141,7 @@
          * @return A variable font family. null if a variable font cannot be built from the given
          *         fonts.
          */
+        @FlaggedApi(FLAG_DEPRECATE_FONTS_XML)
         public @Nullable FontFamily buildVariableFamily() {
             int variableFamilyType = analyzeAndResolveVariableType(mFonts);
             if (variableFamilyType == VARIABLE_FONT_FAMILY_TYPE_UNKNOWN) {
diff --git a/graphics/java/android/graphics/text/LineBreakConfig.java b/graphics/java/android/graphics/text/LineBreakConfig.java
index 13540e0..e81525f 100644
--- a/graphics/java/android/graphics/text/LineBreakConfig.java
+++ b/graphics/java/android/graphics/text/LineBreakConfig.java
@@ -110,6 +110,7 @@
      * This value is resolved to {@link #LINE_BREAK_STYLE_NONE} if this value is used for text
      * layout/rendering.
      */
+    @FlaggedApi(FLAG_NO_BREAK_NO_HYPHENATION_SPAN)
     public static final int LINE_BREAK_STYLE_UNSPECIFIED = -1;
 
     /**
@@ -165,6 +166,7 @@
      * This value is resolved to {@link #LINE_BREAK_WORD_STYLE_NONE} if this value is used for
      * text layout/rendering.
      */
+    @FlaggedApi(FLAG_NO_BREAK_NO_HYPHENATION_SPAN)
     public static final int LINE_BREAK_WORD_STYLE_UNSPECIFIED = -1;
 
     /**
@@ -236,6 +238,7 @@
          * @param config an override line break config
          * @return This {@code Builder}.
          */
+        @FlaggedApi(FLAG_NO_BREAK_NO_HYPHENATION_SPAN)
         public @NonNull Builder merge(@NonNull LineBreakConfig config) {
             if (config.mLineBreakStyle != LINE_BREAK_STYLE_UNSPECIFIED) {
                 mLineBreakStyle = config.mLineBreakStyle;
@@ -483,6 +486,7 @@
      * @param config an overriding config.
      * @return newly created instance that is current style merged with passed config.
      */
+    @FlaggedApi(FLAG_NO_BREAK_NO_HYPHENATION_SPAN)
     public @NonNull LineBreakConfig merge(@NonNull LineBreakConfig config) {
         return new LineBreakConfig(
                 config.mLineBreakStyle == LINE_BREAK_STYLE_UNSPECIFIED
diff --git a/graphics/java/android/graphics/text/LineBreaker.java b/graphics/java/android/graphics/text/LineBreaker.java
index 34ab833..dc2e794 100644
--- a/graphics/java/android/graphics/text/LineBreaker.java
+++ b/graphics/java/android/graphics/text/LineBreaker.java
@@ -16,6 +16,9 @@
 
 package android.graphics.text;
 
+import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH;
+
+import android.annotation.FlaggedApi;
 import android.annotation.FloatRange;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
@@ -248,6 +251,7 @@
          * @see Layout#getUseBoundsForWidth()
          * @see StaticLayout.Builder#setUseBoundsForWidth(boolean)
          */
+        @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH)
         public @NonNull Builder setUseBoundsForWidth(boolean useBoundsForWidth) {
             mUseBoundsForWidth = useBoundsForWidth;
             return this;
diff --git a/graphics/java/android/graphics/text/PositionedGlyphs.java b/graphics/java/android/graphics/text/PositionedGlyphs.java
index 49e9d0c..569f9b6 100644
--- a/graphics/java/android/graphics/text/PositionedGlyphs.java
+++ b/graphics/java/android/graphics/text/PositionedGlyphs.java
@@ -16,6 +16,9 @@
 
 package android.graphics.text;
 
+import static com.android.text.flags.Flags.FLAG_DEPRECATE_FONTS_XML;
+
+import android.annotation.FlaggedApi;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.graphics.Paint;
@@ -170,6 +173,7 @@
      * @param index the glyph index
      * @return true if the fake bold option is on, otherwise off.
      */
+    @FlaggedApi(FLAG_DEPRECATE_FONTS_XML)
     public boolean getFakeBold(@IntRange(from = 0) int index) {
         Preconditions.checkArgumentInRange(index, 0, glyphCount() - 1, "index");
         return nGetFakeBold(mLayoutPtr, index);
@@ -181,6 +185,7 @@
      * @param index the glyph index
      * @return true if the fake italic option is on, otherwise off.
      */
+    @FlaggedApi(FLAG_DEPRECATE_FONTS_XML)
     public boolean getFakeItalic(@IntRange(from = 0) int index) {
         Preconditions.checkArgumentInRange(index, 0, glyphCount() - 1, "index");
         return nGetFakeItalic(mLayoutPtr, index);
@@ -190,6 +195,7 @@
      * A special value returned by {@link #getWeightOverride(int)} and
      * {@link #getItalicOverride(int)} that indicates no font variation setting is overridden.
      */
+    @FlaggedApi(FLAG_DEPRECATE_FONTS_XML)
     public static final float NO_OVERRIDE = Float.MIN_VALUE;
 
     /**
@@ -199,6 +205,7 @@
      * @param index the glyph index
      * @return overridden weight value or {@link #NO_OVERRIDE}.
      */
+    @FlaggedApi(FLAG_DEPRECATE_FONTS_XML)
     public float getWeightOverride(@IntRange(from = 0) int index) {
         Preconditions.checkArgumentInRange(index, 0, glyphCount() - 1, "index");
         float value = nGetWeightOverride(mLayoutPtr, index);
@@ -216,6 +223,7 @@
      * @param index the glyph index
      * @return overridden weight value or {@link #NO_OVERRIDE}.
      */
+    @FlaggedApi(FLAG_DEPRECATE_FONTS_XML)
     public float getItalicOverride(@IntRange(from = 0) int index) {
         Preconditions.checkArgumentInRange(index, 0, glyphCount() - 1, "index");
         float value = nGetItalicOverride(mLayoutPtr, index);
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/compatui/UserAspectRatioSettingsLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayout.java
index 5eeb3b6..b141beb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayout.java
@@ -18,12 +18,16 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.annotation.IdRes;
 import android.annotation.NonNull;
 import android.content.Context;
 import android.util.AttributeSet;
 import android.view.View;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.PathInterpolator;
 import android.widget.ImageButton;
 import android.widget.LinearLayout;
 import android.widget.TextView;
@@ -36,14 +40,29 @@
  */
 public class UserAspectRatioSettingsLayout extends LinearLayout {
 
+    private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
+
+    private static final Interpolator PATH_INTERPOLATOR =
+            new PathInterpolator(0.2f, 0f, 0f, 1f);
+
     private static final float ALPHA_FULL_TRANSPARENT = 0f;
 
     private static final float ALPHA_FULL_OPAQUE = 1f;
 
-    private static final long VISIBILITY_ANIMATION_DURATION_MS = 50;
+    private static final float SCALE_START = 0.8f;
+
+    private static final float SCALE_END = 1f;
+
+    private static final long FADE_ANIMATION_DURATION_MS = 167;
+
+    private static final long SCALE_ANIMATION_DURATION_MS = 300;
 
     private static final String ALPHA_PROPERTY_NAME = "alpha";
 
+    private static final String SCALE_X_PROPERTY_NAME = "scaleX";
+
+    private static final String SCALE_Y_PROPERTY_NAME = "scaleY";
+
     private UserAspectRatioSettingsWindowManager mWindowManager;
 
     public UserAspectRatioSettingsLayout(Context context) {
@@ -88,7 +107,7 @@
         if (show) {
             showItem(view);
         } else {
-            view.setVisibility(visibility);
+            hideItem(view);
         }
     }
 
@@ -121,16 +140,40 @@
     }
 
     private void showItem(@NonNull View view) {
-        view.setVisibility(View.VISIBLE);
+        final AnimatorSet animatorSet = new AnimatorSet();
         final ObjectAnimator fadeIn = ObjectAnimator.ofFloat(view, ALPHA_PROPERTY_NAME,
                 ALPHA_FULL_TRANSPARENT, ALPHA_FULL_OPAQUE);
-        fadeIn.setDuration(VISIBILITY_ANIMATION_DURATION_MS);
-        fadeIn.addListener(new AnimatorListenerAdapter() {
+        fadeIn.setDuration(FADE_ANIMATION_DURATION_MS);
+        fadeIn.setInterpolator(LINEAR_INTERPOLATOR);
+        final ObjectAnimator scaleY =
+                ObjectAnimator.ofFloat(view, SCALE_Y_PROPERTY_NAME, SCALE_START, SCALE_END);
+        final ObjectAnimator scaleX =
+                ObjectAnimator.ofFloat(view, SCALE_X_PROPERTY_NAME, SCALE_START, SCALE_END);
+        scaleX.setDuration(SCALE_ANIMATION_DURATION_MS);
+        scaleX.setInterpolator(PATH_INTERPOLATOR);
+        scaleY.setDuration(SCALE_ANIMATION_DURATION_MS);
+        scaleY.setInterpolator(PATH_INTERPOLATOR);
+        animatorSet.addListener(new AnimatorListenerAdapter() {
             @Override
-            public void onAnimationEnd(Animator animation) {
+            public void onAnimationStart(Animator animation) {
                 view.setVisibility(View.VISIBLE);
             }
         });
-        fadeIn.start();
+        animatorSet.playTogether(fadeIn, scaleY, scaleX);
+        animatorSet.start();
+    }
+
+    private void hideItem(@NonNull View view) {
+        final ObjectAnimator fadeOut = ObjectAnimator.ofFloat(view, ALPHA_PROPERTY_NAME,
+                ALPHA_FULL_OPAQUE, ALPHA_FULL_TRANSPARENT);
+        fadeOut.setDuration(FADE_ANIMATION_DURATION_MS);
+        fadeOut.setInterpolator(LINEAR_INTERPOLATOR);
+        fadeOut.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                view.setVisibility(View.GONE);
+            }
+        });
+        fadeOut.start();
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index f8dd208..09ba4f7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -37,6 +37,7 @@
 import android.view.WindowManager.TRANSIT_NONE
 import android.view.WindowManager.TRANSIT_OPEN
 import android.view.WindowManager.TRANSIT_TO_FRONT
+import android.window.RemoteTransition
 import android.window.TransitionInfo
 import android.window.TransitionRequestInfo
 import android.window.WindowContainerTransaction
@@ -65,7 +66,9 @@
 import com.android.wm.shell.sysui.ShellController
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.sysui.ShellSharedConstants
+import com.android.wm.shell.transition.OneShotRemoteHandler
 import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TransitionHandler
 import com.android.wm.shell.util.KtProtoLog
 import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
 import com.android.wm.shell.windowdecor.MoveToDesktopAnimator
@@ -139,20 +142,22 @@
     }
 
     /** Show all tasks, that are part of the desktop, on top of launcher */
-    fun showDesktopApps(displayId: Int) {
+    fun showDesktopApps(displayId: Int, remoteTransition: RemoteTransition? = null) {
         KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: showDesktopApps")
         val wct = WindowContainerTransaction()
-        // TODO(b/278084491): pass in display id
         bringDesktopAppsToFront(displayId, wct)
 
-        // Execute transaction if there are pending operations
-        if (!wct.isEmpty) {
-            if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-                // TODO(b/268662477): add animation for the transition
-                transitions.startTransition(TRANSIT_NONE, wct, null /* handler */)
-            } else {
-                shellTaskOrganizer.applyTransaction(wct)
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            // TODO(b/255649902): ensure remote transition is supplied once state is introduced
+            val transitionType = if (remoteTransition == null) TRANSIT_NONE else TRANSIT_TO_FRONT
+            val handler = remoteTransition?.let {
+                OneShotRemoteHandler(transitions.mainExecutor, remoteTransition)
             }
+            transitions.startTransition(transitionType, wct, handler).also { t ->
+                handler?.setTransition(t)
+            }
+        } else {
+            shellTaskOrganizer.applyTransaction(wct)
         }
     }
 
@@ -1093,11 +1098,11 @@
             controller = null
         }
 
-        override fun showDesktopApps(displayId: Int) {
+        override fun showDesktopApps(displayId: Int, remoteTransition: RemoteTransition?) {
             ExecutorUtils.executeRemoteCallWithTaskPermission(
                 controller,
                 "showDesktopApps"
-            ) { c -> c.showDesktopApps(displayId) }
+            ) { c -> c.showDesktopApps(displayId, remoteTransition) }
         }
 
         override fun stashDesktopApps(displayId: Int) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
index 47edfd4..6bdaf1e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
@@ -17,6 +17,7 @@
 package com.android.wm.shell.desktopmode;
 
 import android.app.ActivityManager.RunningTaskInfo;
+import android.window.RemoteTransition;
 import com.android.wm.shell.desktopmode.IDesktopTaskListener;
 
 /**
@@ -25,7 +26,7 @@
 interface IDesktopMode {
 
     /** Show apps on the desktop on the given display */
-    void showDesktopApps(int displayId);
+    void showDesktopApps(int displayId, in RemoteTransition remoteTransition);
 
     /** Stash apps on the desktop to allow launching another app from home screen */
     void stashDesktopApps(int displayId);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java
index 0d77a2e..ef8393c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java
@@ -29,12 +29,16 @@
 import android.graphics.Insets;
 import android.graphics.Rect;
 import android.graphics.Region;
+import android.os.Handler;
+import android.os.Looper;
 import android.view.SurfaceControl;
 import android.view.SurfaceHolder;
 import android.view.SurfaceView;
 import android.view.View;
 import android.view.ViewTreeObserver;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.util.concurrent.Executor;
 
 /**
@@ -74,6 +78,7 @@
     private final TaskViewTaskController mTaskViewTaskController;
     private Region mObscuredTouchRegion;
     private Insets mCaptionInsets;
+    private Handler mHandler;
 
     public TaskView(Context context, TaskViewTaskController taskViewTaskController) {
         super(context, null, 0, 0, true /* disableBackgroundLayer */);
@@ -81,6 +86,7 @@
         // TODO(b/266736992): Think about a better way to set the TaskViewBase on the
         //  TaskViewTaskController and vice-versa
         mTaskViewTaskController.setTaskViewBase(this);
+        mHandler = Handler.getMain();
         getHolder().addCallback(this);
     }
 
@@ -117,14 +123,16 @@
     public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
         onLocationChanged();
         if (taskInfo.taskDescription != null) {
-            setResizeBackgroundColor(taskInfo.taskDescription.getBackgroundColor());
+            final int bgColor = taskInfo.taskDescription.getBackgroundColor();
+            runOnViewThread(() -> setResizeBackgroundColor(bgColor));
         }
     }
 
     @Override
     public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
         if (taskInfo.taskDescription != null) {
-            setResizeBackgroundColor(taskInfo.taskDescription.getBackgroundColor());
+            final int bgColor = taskInfo.taskDescription.getBackgroundColor();
+            runOnViewThread(() -> setResizeBackgroundColor(bgColor));
         }
     }
 
@@ -143,7 +151,7 @@
 
     @Override
     public void setResizeBgColor(SurfaceControl.Transaction t, int bgColor) {
-        setResizeBackgroundColor(t, bgColor);
+        runOnViewThread(() -> setResizeBackgroundColor(t, bgColor));
     }
 
     /**
@@ -272,12 +280,14 @@
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         getViewTreeObserver().addOnComputeInternalInsetsListener(this);
+        mHandler = getHandler();
     }
 
     @Override
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
         getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
+        mHandler = Handler.getMain();
     }
 
     /** Returns the task info for the task in the TaskView. */
@@ -285,4 +295,24 @@
     public ActivityManager.RunningTaskInfo getTaskInfo() {
         return mTaskViewTaskController.getTaskInfo();
     }
+
+    /**
+     * Sets the handler, only for testing.
+     */
+    @VisibleForTesting
+    void setHandler(Handler viewHandler) {
+        mHandler = viewHandler;
+    }
+
+    /**
+     * Ensures that the given runnable runs on the view's thread.
+     */
+    private void runOnViewThread(Runnable r) {
+        if (mHandler.getLooper().isCurrentThread()) {
+            r.run();
+        } else {
+            // If this call is not from the same thread as the view, then post it
+            mHandler.post(r);
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRemoteTransition.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRemoteTransition.java
new file mode 100644
index 0000000..0df42b3
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRemoteTransition.java
@@ -0,0 +1,59 @@
+/*
+ * 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.wm.shell;
+
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.view.SurfaceControl;
+import android.window.IRemoteTransition;
+import android.window.IRemoteTransitionFinishedCallback;
+import android.window.TransitionInfo;
+import android.window.WindowContainerTransaction;
+
+/**
+ * {@link IRemoteTransition} for testing purposes.
+ * Stores info about
+ * {@link #startAnimation(IBinder, TransitionInfo, SurfaceControl.Transaction,
+ * IRemoteTransitionFinishedCallback)} being called.
+ */
+public class TestRemoteTransition extends IRemoteTransition.Stub {
+    private boolean mCalled = false;
+    final WindowContainerTransaction mRemoteFinishWCT = new WindowContainerTransaction();
+
+    @Override
+    public void startAnimation(IBinder transition, TransitionInfo info,
+            SurfaceControl.Transaction startTransaction,
+            IRemoteTransitionFinishedCallback finishCallback)
+            throws RemoteException {
+        mCalled = true;
+        finishCallback.onTransitionFinished(mRemoteFinishWCT, null /* sct */);
+    }
+
+    @Override
+    public void mergeAnimation(IBinder transition, TransitionInfo info,
+            SurfaceControl.Transaction t, IBinder mergeTarget,
+            IRemoteTransitionFinishedCallback finishCallback) throws RemoteException {
+    }
+
+    /**
+     * Check whether this remote transition
+     * {@link #startAnimation(IBinder, TransitionInfo, SurfaceControl.Transaction,
+     * IRemoteTransitionFinishedCallback)} is called
+     */
+    public boolean isCalled() {
+        return mCalled;
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index c6cccc0..664fbb2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -28,10 +28,10 @@
 import android.view.Display.DEFAULT_DISPLAY
 import android.view.WindowManager
 import android.view.WindowManager.TRANSIT_CHANGE
-import android.view.WindowManager.TRANSIT_NONE
 import android.view.WindowManager.TRANSIT_OPEN
 import android.view.WindowManager.TRANSIT_TO_FRONT
 import android.window.DisplayAreaInfo
+import android.window.RemoteTransition
 import android.window.TransitionRequestInfo
 import android.window.WindowContainerTransaction
 import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER
@@ -43,6 +43,7 @@
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
 import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestRemoteTransition
 import com.android.wm.shell.TestRunningTaskInfoBuilder
 import com.android.wm.shell.TestShellExecutor
 import com.android.wm.shell.common.DisplayController
@@ -57,8 +58,10 @@
 import com.android.wm.shell.sysui.ShellCommandHandler
 import com.android.wm.shell.sysui.ShellController
 import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.transition.OneShotRemoteHandler
 import com.android.wm.shell.transition.Transitions
 import com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS
+import com.android.wm.shell.transition.Transitions.TransitionHandler
 import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
@@ -69,6 +72,7 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.eq
+import org.mockito.ArgumentMatchers.isA
 import org.mockito.ArgumentMatchers.isNull
 import org.mockito.Mock
 import org.mockito.Mockito
@@ -174,9 +178,9 @@
         markTaskHidden(task1)
         markTaskHidden(task2)
 
-        controller.showDesktopApps(DEFAULT_DISPLAY)
+        controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
 
-        val wct = getLatestWct(expectTransition = TRANSIT_NONE)
+        val wct = getLatestWct(type = TRANSIT_OPEN, handlerClass = OneShotRemoteHandler::class.java)
         assertThat(wct.hierarchyOps).hasSize(3)
         // Expect order to be from bottom: home, task1, task2
         wct.assertReorderAt(index = 0, homeTask)
@@ -192,9 +196,9 @@
         markTaskVisible(task1)
         markTaskVisible(task2)
 
-        controller.showDesktopApps(DEFAULT_DISPLAY)
+        controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
 
-        val wct = getLatestWct(expectTransition = TRANSIT_NONE)
+        val wct = getLatestWct(type = TRANSIT_OPEN, handlerClass = OneShotRemoteHandler::class.java)
         assertThat(wct.hierarchyOps).hasSize(3)
         // Expect order to be from bottom: home, task1, task2
         wct.assertReorderAt(index = 0, homeTask)
@@ -210,9 +214,9 @@
         markTaskHidden(task1)
         markTaskVisible(task2)
 
-        controller.showDesktopApps(DEFAULT_DISPLAY)
+        controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
 
-        val wct = getLatestWct(expectTransition = TRANSIT_NONE)
+        val wct = getLatestWct(type = TRANSIT_OPEN, handlerClass = OneShotRemoteHandler::class.java)
         assertThat(wct.hierarchyOps).hasSize(3)
         // Expect order to be from bottom: home, task1, task2
         wct.assertReorderAt(index = 0, homeTask)
@@ -224,9 +228,9 @@
     fun showDesktopApps_noActiveTasks_reorderHomeToTop() {
         val homeTask = setUpHomeTask()
 
-        controller.showDesktopApps(DEFAULT_DISPLAY)
+        controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
 
-        val wct = getLatestWct(expectTransition = TRANSIT_NONE)
+        val wct = getLatestWct(type = TRANSIT_OPEN, handlerClass = OneShotRemoteHandler::class.java)
         assertThat(wct.hierarchyOps).hasSize(1)
         wct.assertReorderAt(index = 0, homeTask)
     }
@@ -240,9 +244,9 @@
         markTaskHidden(taskDefaultDisplay)
         markTaskHidden(taskSecondDisplay)
 
-        controller.showDesktopApps(DEFAULT_DISPLAY)
+        controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
 
-        val wct = getLatestWct(expectTransition = TRANSIT_NONE)
+        val wct = getLatestWct(type = TRANSIT_OPEN, handlerClass = OneShotRemoteHandler::class.java)
         assertThat(wct.hierarchyOps).hasSize(2)
         // Expect order to be from bottom: home, task
         wct.assertReorderAt(index = 0, homeTaskDefaultDisplay)
@@ -373,7 +377,7 @@
         val task = setUpFreeformTask()
         task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FULLSCREEN
         controller.moveToFullscreen(task)
-        val wct = getLatestWct(expectTransition = TRANSIT_CHANGE)
+        val wct = getLatestWct(type = TRANSIT_CHANGE)
         assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
             .isEqualTo(WINDOWING_MODE_UNDEFINED)
     }
@@ -383,7 +387,7 @@
         val task = setUpFreeformTask()
         task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FREEFORM
         controller.moveToFullscreen(task)
-        val wct = getLatestWct(expectTransition = TRANSIT_CHANGE)
+        val wct = getLatestWct(type = TRANSIT_CHANGE)
         assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
                 .isEqualTo(WINDOWING_MODE_FULLSCREEN)
     }
@@ -401,7 +405,7 @@
 
         controller.moveToFullscreen(taskDefaultDisplay)
 
-        with(getLatestWct(expectTransition = TRANSIT_CHANGE)) {
+        with(getLatestWct(type = TRANSIT_CHANGE)) {
             assertThat(changes.keys).contains(taskDefaultDisplay.token.asBinder())
             assertThat(changes.keys).doesNotContain(taskSecondDisplay.token.asBinder())
         }
@@ -414,7 +418,7 @@
 
         controller.moveTaskToFront(task1)
 
-        val wct = getLatestWct(expectTransition = TRANSIT_TO_FRONT)
+        val wct = getLatestWct(type = TRANSIT_TO_FRONT)
         assertThat(wct.hierarchyOps).hasSize(1)
         wct.assertReorderAt(index = 0, task1)
     }
@@ -439,7 +443,7 @@
 
         val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
         controller.moveToNextDisplay(task.taskId)
-        with(getLatestWct(expectTransition = TRANSIT_CHANGE)) {
+        with(getLatestWct(type = TRANSIT_CHANGE)) {
             assertThat(hierarchyOps).hasSize(1)
             assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder())
             assertThat(hierarchyOps[0].isReparent).isTrue()
@@ -461,7 +465,7 @@
         val task = setUpFreeformTask(displayId = SECOND_DISPLAY)
         controller.moveToNextDisplay(task.taskId)
 
-        with(getLatestWct(expectTransition = TRANSIT_CHANGE)) {
+        with(getLatestWct(type = TRANSIT_CHANGE)) {
             assertThat(hierarchyOps).hasSize(1)
             assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder())
             assertThat(hierarchyOps[0].isReparent).isTrue()
@@ -747,11 +751,16 @@
     }
 
     private fun getLatestWct(
-        @WindowManager.TransitionType expectTransition: Int = TRANSIT_OPEN
+            @WindowManager.TransitionType type: Int = TRANSIT_OPEN,
+            handlerClass: Class<out TransitionHandler>? = null
     ): WindowContainerTransaction {
         val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
         if (ENABLE_SHELL_TRANSITIONS) {
-            verify(transitions).startTransition(eq(expectTransition), arg.capture(), isNull())
+            if (handlerClass == null) {
+                verify(transitions).startTransition(eq(type), arg.capture(), isNull())
+            } else {
+                verify(transitions).startTransition(eq(type), arg.capture(), isA(handlerClass))
+            }
         } else {
             verify(shellTaskOrganizer).applyTransaction(arg.capture())
         }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
index 5efd9ad..d542139 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
@@ -47,11 +47,8 @@
 import android.annotation.NonNull;
 import android.app.ActivityManager;
 import android.os.IBinder;
-import android.os.RemoteException;
 import android.view.SurfaceControl;
 import android.view.SurfaceSession;
-import android.window.IRemoteTransition;
-import android.window.IRemoteTransitionFinishedCallback;
 import android.window.RemoteTransition;
 import android.window.TransitionInfo;
 import android.window.TransitionRequestInfo;
@@ -65,6 +62,7 @@
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.TestRemoteTransition;
 import com.android.wm.shell.TestRunningTaskInfoBuilder;
 import com.android.wm.shell.TransitionInfoBuilder;
 import com.android.wm.shell.common.DisplayController;
@@ -205,7 +203,7 @@
 
         // Make sure split-screen is now visible
         assertTrue(mStageCoordinator.isSplitScreenVisible());
-        assertTrue(testRemote.mCalled);
+        assertTrue(testRemote.isCalled());
     }
 
     @Test
@@ -468,24 +466,4 @@
         return out;
     }
 
-    class TestRemoteTransition extends IRemoteTransition.Stub {
-        boolean mCalled = false;
-        final WindowContainerTransaction mRemoteFinishWCT = new WindowContainerTransaction();
-
-        @Override
-        public void startAnimation(IBinder transition, TransitionInfo info,
-                SurfaceControl.Transaction startTransaction,
-                IRemoteTransitionFinishedCallback finishCallback)
-                throws RemoteException {
-            mCalled = true;
-            finishCallback.onTransitionFinished(mRemoteFinishWCT, null /* sct */);
-        }
-
-        @Override
-        public void mergeAnimation(IBinder transition, TransitionInfo info,
-                SurfaceControl.Transaction t, IBinder mergeTarget,
-                IRemoteTransitionFinishedCallback finishCallback) throws RemoteException {
-        }
-    }
-
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java
index 0088051..4afb29e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java
@@ -43,6 +43,8 @@
 import android.graphics.Insets;
 import android.graphics.Rect;
 import android.graphics.Region;
+import android.os.Handler;
+import android.os.Looper;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.SurfaceControl;
@@ -88,6 +90,10 @@
     SyncTransactionQueue mSyncQueue;
     @Mock
     Transitions mTransitions;
+    @Mock
+    Looper mViewLooper;
+    @Mock
+    Handler mViewHandler;
 
     SurfaceSession mSession;
     SurfaceControl mLeash;
@@ -105,6 +111,8 @@
                 .build();
 
         mContext = getContext();
+        doReturn(true).when(mViewLooper).isCurrentThread();
+        doReturn(mViewLooper).when(mViewHandler).getLooper();
 
         mTaskInfo = new ActivityManager.RunningTaskInfo();
         mTaskInfo.token = mToken;
@@ -132,6 +140,7 @@
         mTaskViewTaskController = spy(new TaskViewTaskController(mContext, mOrganizer,
                 mTaskViewTransitions, mSyncQueue));
         mTaskView = new TaskView(mContext, mTaskViewTaskController);
+        mTaskView.setHandler(mViewHandler);
         mTaskView.setListener(mExecutor, mViewListener);
     }
 
@@ -646,4 +655,17 @@
 
         assertThat(mTaskViewTaskController.getTaskInfo()).isNull();
     }
+
+    @Test
+    public void testOnTaskInfoChangedOnSameUiThread() {
+        mTaskViewTaskController.onTaskInfoChanged(mTaskInfo);
+        verify(mViewHandler, never()).post(any());
+    }
+
+    @Test
+    public void testOnTaskInfoChangedOnDifferentUiThread() {
+        doReturn(false).when(mViewLooper).isCurrentThread();
+        mTaskViewTaskController.onTaskInfoChanged(mTaskInfo);
+        verify(mViewHandler).post(any());
+    }
 }
diff --git a/libs/hwui/Mesh.h b/libs/hwui/Mesh.h
index 764d1ef..69fda34 100644
--- a/libs/hwui/Mesh.h
+++ b/libs/hwui/Mesh.h
@@ -166,11 +166,12 @@
 #endif
                 mMesh = SkMesh::MakeIndexed(mMeshSpec, meshMode, vb, mVertexCount, mVertexOffset,
                                             ib, mIndexCount, mIndexOffset, mBuilder->fUniforms,
-                                            mBounds)
+                                            SkSpan<SkRuntimeEffect::ChildPtr>(), mBounds)
                                 .mesh;
             } else {
                 mMesh = SkMesh::Make(mMeshSpec, meshMode, vb, mVertexCount, mVertexOffset,
-                                     mBuilder->fUniforms, mBounds)
+                                     mBuilder->fUniforms, SkSpan<SkRuntimeEffect::ChildPtr>(),
+                                     mBounds)
                                 .mesh;
             }
             mIsDirty = false;
diff --git a/libs/hwui/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp
index 71f47e9..ff0d8d74 100644
--- a/libs/hwui/RecordingCanvas.cpp
+++ b/libs/hwui/RecordingCanvas.cpp
@@ -539,7 +539,7 @@
             if (!cpuMesh.indexBuffer()) {
                 gpuMesh = SkMesh::Make(cpuMesh.refSpec(), cpuMesh.mode(), vb, cpuMesh.vertexCount(),
                                        cpuMesh.vertexOffset(), cpuMesh.refUniforms(),
-                                       cpuMesh.bounds())
+                                       SkSpan<SkRuntimeEffect::ChildPtr>(), cpuMesh.bounds())
                                   .mesh;
             } else {
                 sk_sp<SkMesh::IndexBuffer> ib =
@@ -547,7 +547,8 @@
                 gpuMesh = SkMesh::MakeIndexed(cpuMesh.refSpec(), cpuMesh.mode(), vb,
                                               cpuMesh.vertexCount(), cpuMesh.vertexOffset(), ib,
                                               cpuMesh.indexCount(), cpuMesh.indexOffset(),
-                                              cpuMesh.refUniforms(), cpuMesh.bounds())
+                                              cpuMesh.refUniforms(),
+                                              SkSpan<SkRuntimeEffect::ChildPtr>(), cpuMesh.bounds())
                                   .mesh;
             }
 
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/media/java/android/media/tv/tuner/Tuner.java b/media/java/android/media/tv/tuner/Tuner.java
index 9924fae..09f09b9 100644
--- a/media/java/android/media/tv/tuner/Tuner.java
+++ b/media/java/android/media/tv/tuner/Tuner.java
@@ -2409,13 +2409,16 @@
     @RequiresPermission(android.Manifest.permission.ACCESS_TV_DESCRAMBLER)
     @Nullable
     public Descrambler openDescrambler() {
+        acquireTRMSLock("openDescrambler()");
         mDemuxLock.lock();
         try {
-            if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX, mDemuxLock)) {
+            // no need to unlock mDemuxLock (so pass null instead) as TRMS lock is already acquired
+            if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX, null)) {
                 return null;
             }
             return requestDescrambler();
         } finally {
+            releaseTRMSLock();
             mDemuxLock.unlock();
         }
     }
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/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
index 943c2b4..ba88484 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
@@ -16,23 +16,183 @@
 
 package com.android.credentialmanager.autofill
 
+import android.app.assist.AssistStructure
+import android.content.Context
+import android.credentials.GetCredentialRequest
+import android.credentials.CredentialManager
+import android.credentials.GetCandidateCredentialsResponse
+import android.credentials.CredentialOption
+import android.credentials.GetCandidateCredentialsException
+import android.os.Bundle
 import android.os.CancellationSignal
-import android.service.autofill.AutofillService
-import android.service.autofill.FillCallback
+import android.os.OutcomeReceiver
 import android.service.autofill.FillRequest
+import android.service.autofill.AutofillService
+import android.service.autofill.FillResponse
+import android.service.autofill.FillCallback
 import android.service.autofill.SaveRequest
 import android.service.autofill.SaveCallback
+import android.util.Log
+import org.json.JSONObject
+import java.util.concurrent.Executors
 
 class CredentialAutofillService : AutofillService() {
+
+    companion object {
+        private const val TAG = "CredAutofill"
+
+        private const val CRED_HINT_PREFIX = "credential="
+        private const val REQUEST_DATA_KEY = "requestData"
+        private const val CANDIDATE_DATA_KEY = "candidateQueryData"
+        private const val SYS_PROVIDER_REQ_KEY = "isSystemProviderRequired"
+        private const val CRED_OPTIONS_KEY = "credentialOptions"
+        private const val TYPE_KEY = "type"
+    }
+
+    private val credentialManager: CredentialManager =
+            getSystemService(Context.CREDENTIAL_SERVICE) as CredentialManager
+
     override fun onFillRequest(
             request: FillRequest,
             cancellationSignal: CancellationSignal,
             callback: FillCallback
     ) {
+        val context = request.fillContexts
+        val structure = context[context.size - 1].structure
+        val callingPackage = structure.activityComponent.packageName
+        Log.i(TAG, "onFillRequest called for $callingPackage")
+
+        val getCredRequest: GetCredentialRequest? = getCredManRequest(structure)
+        if (getCredRequest == null) {
+            callback.onFailure("No credential manager request found")
+            return
+        }
+
+        val outcome = object : OutcomeReceiver<GetCandidateCredentialsResponse,
+                GetCandidateCredentialsException> {
+            override fun onResult(result: GetCandidateCredentialsResponse) {
+                Log.i(TAG, "getCandidateCredentials onResponse")
+                val fillResponse: FillResponse? = convertToFillResponse(result, request)
+                callback.onSuccess(fillResponse)
+            }
+
+            override fun onError(error: GetCandidateCredentialsException) {
+                Log.i(TAG, "getCandidateCredentials onError")
+                callback.onFailure("error received from credential manager ${error.message}")
+            }
+        }
+
+        credentialManager.getCandidateCredentials(
+                getCredRequest,
+                callingPackage,
+                CancellationSignal(),
+                Executors.newSingleThreadExecutor(),
+                outcome
+        )
+    }
+
+    private fun convertToFillResponse(
+            getCredResponse: GetCandidateCredentialsResponse,
+            filLRequest: FillRequest
+    ): FillResponse? {
         TODO("Not yet implemented")
     }
 
     override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
         TODO("Not yet implemented")
     }
+
+    private fun getCredManRequest(structure: AssistStructure): GetCredentialRequest? {
+        val credentialOptions: MutableList<CredentialOption> = mutableListOf()
+        traverseStructure(structure, credentialOptions)
+
+        if (credentialOptions.isNotEmpty()) {
+            return GetCredentialRequest.Builder(Bundle.EMPTY)
+                    .setCredentialOptions(credentialOptions)
+                    .build()
+        }
+        return null
+    }
+
+    private fun traverseStructure(
+            structure: AssistStructure,
+            cmRequests: MutableList<CredentialOption>
+    ) {
+        val windowNodes: List<AssistStructure.WindowNode> =
+                structure.run {
+                    (0 until windowNodeCount).map { getWindowNodeAt(it) }
+                }
+
+        windowNodes.forEach { windowNode: AssistStructure.WindowNode ->
+            traverseNode(windowNode.rootViewNode, cmRequests)
+        }
+    }
+
+    private fun traverseNode(
+            viewNode: AssistStructure.ViewNode?,
+            cmRequests: MutableList<CredentialOption>
+    ) {
+        val options = getCredentialOptionsFromViewNode(viewNode)
+        cmRequests.addAll(options)
+
+        val children: List<AssistStructure.ViewNode>? =
+                viewNode?.run {
+                    (0 until childCount).map { getChildAt(it) }
+                }
+
+        children?.forEach { childNode: AssistStructure.ViewNode ->
+            traverseNode(childNode, cmRequests)
+        }
+    }
+
+    private fun getCredentialOptionsFromViewNode(viewNode: AssistStructure.ViewNode?):
+            List<CredentialOption> {
+        // TODO(b/293945193) Replace with isCredential check from viewNode
+        val credentialHints: MutableList<String> = mutableListOf()
+        if (viewNode != null && viewNode.autofillHints != null) {
+            for (hint in viewNode.autofillHints!!) {
+                if (hint.startsWith(CRED_HINT_PREFIX)) {
+                    credentialHints.add(hint.substringAfter(CRED_HINT_PREFIX))
+                }
+            }
+        }
+
+        val credentialOptions: MutableList<CredentialOption> = mutableListOf()
+        for (credentialHint in credentialHints) {
+            convertJsonToCredentialOption(credentialHint).let { credentialOptions.addAll(it) }
+        }
+        return credentialOptions
+    }
+
+    private fun convertJsonToCredentialOption(jsonString: String): List<CredentialOption> {
+        // TODO(b/302000646) Move this logic to jetpack so that is consistent
+        //  with building the json
+        val credentialOptions: MutableList<CredentialOption> = mutableListOf()
+
+        val json = JSONObject(jsonString)
+        val options = json.getJSONArray(CRED_OPTIONS_KEY)
+        for (i in 0 until options.length()) {
+            val option = options.getJSONObject(i)
+
+            credentialOptions.add(CredentialOption(
+                    option.getString(TYPE_KEY),
+                    convertJsonToBundle(option.getJSONObject(REQUEST_DATA_KEY)),
+                    convertJsonToBundle(option.getJSONObject(CANDIDATE_DATA_KEY)),
+                    option.getBoolean(SYS_PROVIDER_REQ_KEY),
+            ))
+        }
+        return credentialOptions
+    }
+
+    private fun convertJsonToBundle(json: JSONObject): Bundle {
+        val result = Bundle()
+        json.keys().forEach {
+            val v = json.get(it)
+            when (v) {
+                is String -> result.putString(it, v)
+                is Boolean -> result.putBoolean(it, v)
+            }
+        }
+        return result
+    }
 }
\ No newline at end of file
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/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp
index 5dcb9d2..2d231f2 100644
--- a/packages/SettingsLib/Android.bp
+++ b/packages/SettingsLib/Android.bp
@@ -71,7 +71,6 @@
         "SettingsLibMainSwitchPreference",
         "SettingsLibProfileSelector",
         "SettingsLibProgressBar",
-        "SettingsLibRadioButtonPreference",
         "SettingsLibRestrictedLockUtils",
         "SettingsLibSelectorWithWidgetPreference",
         "SettingsLibSettingsSpinner",
diff --git a/packages/SettingsLib/RadioButtonPreference/Android.bp b/packages/SettingsLib/RadioButtonPreference/Android.bp
deleted file mode 100644
index 505ba05..0000000
--- a/packages/SettingsLib/RadioButtonPreference/Android.bp
+++ /dev/null
@@ -1,29 +0,0 @@
-package {
-    // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "frameworks_base_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    default_applicable_licenses: ["frameworks_base_license"],
-}
-
-android_library {
-    name: "SettingsLibRadioButtonPreference",
-    use_resource_processor: true,
-
-    srcs: ["src/**/*.java"],
-    resource_dirs: ["res"],
-
-    static_libs: [
-          "androidx.preference_preference",
-          "SettingsLibSelectorWithWidgetPreference",
-          "SettingsLibSettingsTheme",
-    ],
-
-    sdk_version: "system_current",
-    min_sdk_version: "21",
-    apex_available: [
-        "//apex_available:platform",
-        "com.android.permission",
-    ],
-}
diff --git a/packages/SettingsLib/RadioButtonPreference/AndroidManifest.xml b/packages/SettingsLib/RadioButtonPreference/AndroidManifest.xml
deleted file mode 100644
index 8b5c3b1..0000000
--- a/packages/SettingsLib/RadioButtonPreference/AndroidManifest.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  Copyright (C) 2019 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.
--->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="com.android.settingslib.widget.preference.radio">
-
-    <uses-sdk android:minSdkVersion="21" />
-
-</manifest>
diff --git a/packages/SettingsLib/RadioButtonPreference/res/drawable/ic_settings_accent.xml b/packages/SettingsLib/RadioButtonPreference/res/drawable/ic_settings_accent.xml
deleted file mode 100644
index 6521bc9..0000000
--- a/packages/SettingsLib/RadioButtonPreference/res/drawable/ic_settings_accent.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<!--
-  Copyright (C) 2021 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.
-  -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="24dp"
-        android:height="24dp"
-        android:viewportWidth="24.0"
-        android:viewportHeight="24.0"
-        android:tint="?android:attr/colorAccent">
-    <path
-        android:fillColor="#FFFFFFFF"
-        android:pathData="M13.85,22.25h-3.7c-0.74,0 -1.36,-0.54 -1.45,-1.27l-0.27,-1.89c-0.27,-0.14 -0.53,-0.29 -0.79,-0.46l-1.8,0.72c-0.7,0.26 -1.47,-0.03 -1.81,-0.65L2.2,15.53c-0.35,-0.66 -0.2,-1.44 0.36,-1.88l1.53,-1.19c-0.01,-0.15 -0.02,-0.3 -0.02,-0.46c0,-0.15 0.01,-0.31 0.02,-0.46l-1.52,-1.19C1.98,9.9 1.83,9.09 2.2,8.47l1.85,-3.19c0.34,-0.62 1.11,-0.9 1.79,-0.63l1.81,0.73c0.26,-0.17 0.52,-0.32 0.78,-0.46l0.27,-1.91c0.09,-0.7 0.71,-1.25 1.44,-1.25h3.7c0.74,0 1.36,0.54 1.45,1.27l0.27,1.89c0.27,0.14 0.53,0.29 0.79,0.46l1.8,-0.72c0.71,-0.26 1.48,0.03 1.82,0.65l1.84,3.18c0.36,0.66 0.2,1.44 -0.36,1.88l-1.52,1.19c0.01,0.15 0.02,0.3 0.02,0.46s-0.01,0.31 -0.02,0.46l1.52,1.19c0.56,0.45 0.72,1.23 0.37,1.86l-1.86,3.22c-0.34,0.62 -1.11,0.9 -1.8,0.63l-1.8,-0.72c-0.26,0.17 -0.52,0.32 -0.78,0.46l-0.27,1.91C15.21,21.71 14.59,22.25 13.85,22.25zM13.32,20.72c0,0.01 0,0.01 0,0.02L13.32,20.72zM10.68,20.7l0,0.02C10.69,20.72 10.69,20.71 10.68,20.7zM10.62,20.25h2.76l0.37,-2.55l0.53,-0.22c0.44,-0.18 0.88,-0.44 1.34,-0.78l0.45,-0.34l2.38,0.96l1.38,-2.4l-2.03,-1.58l0.07,-0.56c0.03,-0.26 0.06,-0.51 0.06,-0.78c0,-0.27 -0.03,-0.53 -0.06,-0.78l-0.07,-0.56l2.03,-1.58l-1.39,-2.4l-2.39,0.96l-0.45,-0.35c-0.42,-0.32 -0.87,-0.58 -1.33,-0.77L13.75,6.3l-0.37,-2.55h-2.76L10.25,6.3L9.72,6.51C9.28,6.7 8.84,6.95 8.38,7.3L7.93,7.63L5.55,6.68L4.16,9.07l2.03,1.58l-0.07,0.56C6.09,11.47 6.06,11.74 6.06,12c0,0.26 0.02,0.53 0.06,0.78l0.07,0.56l-2.03,1.58l1.38,2.4l2.39,-0.96l0.45,0.35c0.43,0.33 0.86,0.58 1.33,0.77l0.53,0.22L10.62,20.25zM18.22,17.72c0,0.01 -0.01,0.02 -0.01,0.03L18.22,17.72zM5.77,17.71l0.01,0.02C5.78,17.72 5.77,17.71 5.77,17.71zM3.93,9.47L3.93,9.47C3.93,9.47 3.93,9.47 3.93,9.47zM18.22,6.27c0,0.01 0.01,0.02 0.01,0.02L18.22,6.27zM5.79,6.25L5.78,6.27C5.78,6.27 5.79,6.26 5.79,6.25zM13.31,3.28c0,0.01 0,0.01 0,0.02L13.31,3.28zM10.69,3.26l0,0.02C10.69,3.27 10.69,3.27 10.69,3.26z"/>
-    <path
-        android:fillColor="#FFFFFFFF"
-        android:pathData="M12,12m-3.5,0a3.5,3.5 0,1 1,7 0a3.5,3.5 0,1 1,-7 0"/>
-</vector>
\ No newline at end of file
diff --git a/packages/SettingsLib/RadioButtonPreference/res/layout-v33/preference_radio.xml b/packages/SettingsLib/RadioButtonPreference/res/layout-v33/preference_radio.xml
deleted file mode 100644
index bb8ac28..0000000
--- a/packages/SettingsLib/RadioButtonPreference/res/layout-v33/preference_radio.xml
+++ /dev/null
@@ -1,127 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  Copyright (C) 2022 The Android Open Source Project
-
-  Licensed under the Apache License, Version 2.0 (the "License");
-  you may not use this file except in compliance with the License.
-  You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-  Unless required by applicable law or agreed to in writing, software
-  distributed under the License is distributed on an "AS IS" BASIS,
-  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  See the License for the specific language governing permissions and
-  limitations under the License.
-  -->
-
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:settings="http://schemas.android.com/apk/res-auto"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:background="?android:attr/selectableItemBackground"
-    android:gravity="center_vertical"
-    android:minHeight="?android:attr/listPreferredItemHeightSmall"
-    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
-
-    <LinearLayout
-        android:id="@android:id/widget_frame"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:paddingHorizontal="20dp"
-        android:gravity="center"
-        android:minWidth="56dp"
-        android:orientation="vertical"/>
-
-    <LinearLayout
-        android:id="@+id/icon_frame"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:gravity="center_vertical"
-        android:minWidth="32dp"
-        android:orientation="horizontal"
-        android:layout_marginEnd="16dp"
-        android:paddingTop="4dp"
-        android:paddingBottom="4dp">
-        <androidx.preference.internal.PreferenceImageView
-            android:id="@android:id/icon"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            settings:maxWidth="@dimen/secondary_app_icon_size"
-            settings:maxHeight="@dimen/secondary_app_icon_size"/>
-    </LinearLayout>
-
-    <LinearLayout
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:layout_weight="1"
-        android:orientation="vertical"
-        android:paddingTop="16dp"
-        android:paddingBottom="16dp"
-        android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
-
-        <TextView
-            android:id="@android:id/title"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:maxLines="2"
-            android:hyphenationFrequency="normalFast"
-            android:lineBreakWordStyle="phrase"
-            android:textAppearance="?android:attr/textAppearanceListItem"/>
-
-        <LinearLayout
-            android:id="@+id/summary_container"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:visibility="gone">
-            <TextView
-                android:id="@android:id/summary"
-                android:layout_width="0dp"
-                android:layout_height="wrap_content"
-                android:layout_weight="1"
-                android:textAppearance="?android:attr/textAppearanceSmall"
-                android:textAlignment="viewStart"
-                android:hyphenationFrequency="normalFast"
-                android:lineBreakWordStyle="phrase"
-                android:textColor="?android:attr/textColorSecondary"/>
-
-            <TextView
-                android:id="@+id/appendix"
-                android:layout_width="0dp"
-                android:layout_height="wrap_content"
-                android:layout_weight="1"
-                android:textAppearance="?android:attr/textAppearanceSmall"
-                android:textAlignment="viewEnd"
-                android:textColor="?android:attr/textColorSecondary"
-                android:maxLines="1"
-                android:visibility="gone"
-                android:ellipsize="end"/>
-        </LinearLayout>
-    </LinearLayout>
-
-    <LinearLayout
-        android:id="@+id/radio_extra_widget_container"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:orientation="horizontal"
-        android:gravity="center_vertical">
-        <View
-            android:layout_width=".75dp"
-            android:layout_height="32dp"
-            android:layout_marginTop="16dp"
-            android:layout_marginBottom="16dp"
-            android:background="?android:attr/dividerVertical" />
-        <ImageView
-            android:id="@+id/radio_extra_widget"
-            android:layout_width="match_parent"
-            android:minWidth="@dimen/two_target_min_width"
-            android:layout_height="fill_parent"
-            android:src="@drawable/ic_settings_accent"
-            android:contentDescription="@string/settings_label"
-            android:paddingStart="24dp"
-            android:paddingEnd="24dp"
-            android:layout_gravity="center"
-            android:background="?android:attr/selectableItemBackground" />
-    </LinearLayout>
-</LinearLayout>
diff --git a/packages/SettingsLib/RadioButtonPreference/res/layout/preference_radio.xml b/packages/SettingsLib/RadioButtonPreference/res/layout/preference_radio.xml
deleted file mode 100644
index 906ff2c..0000000
--- a/packages/SettingsLib/RadioButtonPreference/res/layout/preference_radio.xml
+++ /dev/null
@@ -1,123 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  Copyright (C) 2019 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.
-  -->
-
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:settings="http://schemas.android.com/apk/res-auto"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:background="?android:attr/selectableItemBackground"
-    android:gravity="center_vertical"
-    android:minHeight="?android:attr/listPreferredItemHeightSmall"
-    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
-
-    <LinearLayout
-        android:id="@android:id/widget_frame"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:paddingHorizontal="20dp"
-        android:gravity="center"
-        android:minWidth="56dp"
-        android:orientation="vertical"/>
-
-    <LinearLayout
-        android:id="@+id/icon_frame"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:gravity="center_vertical"
-        android:minWidth="32dp"
-        android:orientation="horizontal"
-        android:layout_marginEnd="16dp"
-        android:paddingTop="4dp"
-        android:paddingBottom="4dp">
-        <androidx.preference.internal.PreferenceImageView
-            android:id="@android:id/icon"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            settings:maxWidth="@dimen/secondary_app_icon_size"
-            settings:maxHeight="@dimen/secondary_app_icon_size"/>
-    </LinearLayout>
-
-    <LinearLayout
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:layout_weight="1"
-        android:orientation="vertical"
-        android:paddingTop="16dp"
-        android:paddingBottom="16dp"
-        android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
-
-        <TextView
-            android:id="@android:id/title"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:maxLines="2"
-            android:textAppearance="?android:attr/textAppearanceListItem"/>
-
-        <LinearLayout
-            android:id="@+id/summary_container"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:visibility="gone">
-            <TextView
-                android:id="@android:id/summary"
-                android:layout_width="0dp"
-                android:layout_height="wrap_content"
-                android:layout_weight="1"
-                android:textAppearance="?android:attr/textAppearanceSmall"
-                android:textAlignment="viewStart"
-                android:textColor="?android:attr/textColorSecondary"/>
-
-            <TextView
-                android:id="@+id/appendix"
-                android:layout_width="0dp"
-                android:layout_height="wrap_content"
-                android:layout_weight="1"
-                android:textAppearance="?android:attr/textAppearanceSmall"
-                android:textAlignment="viewEnd"
-                android:textColor="?android:attr/textColorSecondary"
-                android:maxLines="1"
-                android:visibility="gone"
-                android:ellipsize="end"/>
-        </LinearLayout>
-    </LinearLayout>
-
-    <LinearLayout
-        android:id="@+id/radio_extra_widget_container"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:orientation="horizontal"
-        android:gravity="center_vertical">
-        <View
-            android:layout_width=".75dp"
-            android:layout_height="32dp"
-            android:layout_marginTop="16dp"
-            android:layout_marginBottom="16dp"
-            android:background="?android:attr/dividerVertical" />
-        <ImageView
-            android:id="@+id/radio_extra_widget"
-            android:layout_width="match_parent"
-            android:minWidth="@dimen/two_target_min_width"
-            android:layout_height="fill_parent"
-            android:src="@drawable/ic_settings_accent"
-            android:contentDescription="@string/settings_label"
-            android:paddingStart="24dp"
-            android:paddingEnd="24dp"
-            android:layout_gravity="center"
-            android:background="?android:attr/selectableItemBackground" />
-    </LinearLayout>
-</LinearLayout>
diff --git a/packages/SettingsLib/RadioButtonPreference/res/layout/preference_widget_radiobutton.xml b/packages/SettingsLib/RadioButtonPreference/res/layout/preference_widget_radiobutton.xml
deleted file mode 100644
index cb7b8eb..0000000
--- a/packages/SettingsLib/RadioButtonPreference/res/layout/preference_widget_radiobutton.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  Copyright (C) 2019 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.
-  -->
-
-<RadioButton xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@android:id/checkbox"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content"
-    android:layout_gravity="center"
-    android:background="@null"
-    android:focusable="false"
-    android:clickable="false" />
diff --git a/packages/SettingsLib/RadioButtonPreference/res/values/strings.xml b/packages/SettingsLib/RadioButtonPreference/res/values/strings.xml
deleted file mode 100644
index ff3f90c..0000000
--- a/packages/SettingsLib/RadioButtonPreference/res/values/strings.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  Copyright (C) 2021 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 xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-
-    <!-- Content description for RadioButton with extra gear icon [CHAR LIMIT=NONE] -->
-    <string name="settings_label">Settings</string>
-
-</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/RadioButtonPreference/src/com/android/settingslib/widget/RadioButtonPreference.java b/packages/SettingsLib/RadioButtonPreference/src/com/android/settingslib/widget/RadioButtonPreference.java
deleted file mode 100644
index fc4b714..0000000
--- a/packages/SettingsLib/RadioButtonPreference/src/com/android/settingslib/widget/RadioButtonPreference.java
+++ /dev/null
@@ -1,194 +0,0 @@
-/*
- * Copyright (C) 2019 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.settingslib.widget;
-
-import android.content.Context;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.ImageView;
-
-import androidx.preference.CheckBoxPreference;
-import androidx.preference.PreferenceViewHolder;
-
-import com.android.settingslib.widget.preference.radio.R;
-
-/**
- * DEPRECATED. Please use SelectorWithWidgetPreference instead.
- *
- * This file has been moved there and will be removed once all callers are updated.
- *
- * Check box preference with check box replaced by radio button.
- *
- * Functionally speaking, it's actually a CheckBoxPreference. We only modified
- * the widget to RadioButton to make it "look like" a RadioButtonPreference.
- *
- * In other words, there's no "RadioButtonPreferenceGroup" in this
- * implementation. When you check one RadioButtonPreference, if you want to
- * uncheck all the other preferences, you should do that by code yourself.
- *
- * RadioButtonPreference can assign a extraWidgetListener to show a gear icon
- * on the right side that can open another page.
- *
- * @Deprecated
- */
-public class RadioButtonPreference extends CheckBoxPreference {
-
-    /**
-     * Interface definition for a callback to be invoked when the preference is clicked.
-     */
-    public interface OnClickListener {
-        /**
-         * Called when a preference has been clicked.
-         *
-         * @param emiter The clicked preference
-         */
-        void onRadioButtonClicked(RadioButtonPreference emiter);
-    }
-
-    private OnClickListener mListener = null;
-    private View mAppendix;
-    private int mAppendixVisibility = -1;
-
-    private View mExtraWidgetContainer;
-    private ImageView mExtraWidget;
-
-    private View.OnClickListener mExtraWidgetOnClickListener;
-
-    /**
-     * Perform inflation from XML and apply a class-specific base style.
-     *
-     * @param context  The {@link Context} this is associated with, through which it can
-     *                 access the current theme, resources, {@link SharedPreferences}, etc.
-     * @param attrs    The attributes of the XML tag that is inflating the preference
-     * @param defStyle An attribute in the current theme that contains a reference to a style
-     *                 resource that supplies default values for the view. Can be 0 to not
-     *                 look for defaults.
-     */
-    public RadioButtonPreference(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
-        init();
-    }
-
-    /**
-     * Perform inflation from XML and apply a class-specific base style.
-     *
-     * @param context The {@link Context} this is associated with, through which it can
-     *                access the current theme, resources, {@link SharedPreferences}, etc.
-     * @param attrs   The attributes of the XML tag that is inflating the preference
-     */
-    public RadioButtonPreference(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        init();
-    }
-
-    /**
-     * Constructor to create a preference.
-     *
-     * @param context The Context this is associated with.
-     */
-    public RadioButtonPreference(Context context) {
-        this(context, null);
-    }
-
-    /**
-     * Sets the callback to be invoked when this preference is clicked by the user.
-     *
-     * @param listener The callback to be invoked
-     */
-    public void setOnClickListener(OnClickListener listener) {
-        mListener = listener;
-    }
-
-    /**
-     * Processes a click on the preference.
-     */
-    @Override
-    public void onClick() {
-        if (mListener != null) {
-            mListener.onRadioButtonClicked(this);
-        }
-    }
-
-    /**
-     * Binds the created View to the data for this preference.
-     *
-     * <p>This is a good place to grab references to custom Views in the layout and set
-     * properties on them.
-     *
-     * <p>Make sure to call through to the superclass's implementation.
-     *
-     * @param holder The ViewHolder that provides references to the views to fill in. These views
-     *               will be recycled, so you should not hold a reference to them after this method
-     *               returns.
-     */
-    @Override
-    public void onBindViewHolder(PreferenceViewHolder holder) {
-        super.onBindViewHolder(holder);
-
-        View summaryContainer = holder.findViewById(R.id.summary_container);
-        if (summaryContainer != null) {
-            summaryContainer.setVisibility(
-                    TextUtils.isEmpty(getSummary()) ? View.GONE : View.VISIBLE);
-            mAppendix = holder.findViewById(R.id.appendix);
-            if (mAppendix != null && mAppendixVisibility != -1) {
-                mAppendix.setVisibility(mAppendixVisibility);
-            }
-        }
-
-        mExtraWidget = (ImageView) holder.findViewById(R.id.radio_extra_widget);
-        mExtraWidgetContainer = holder.findViewById(R.id.radio_extra_widget_container);
-
-        setExtraWidgetOnClickListener(mExtraWidgetOnClickListener);
-    }
-
-    /**
-     * Set the visibility state of appendix view.
-     *
-     * @param visibility One of {@link View#VISIBLE}, {@link View#INVISIBLE}, or {@link View#GONE}.
-     */
-    public void setAppendixVisibility(int visibility) {
-        if (mAppendix != null) {
-            mAppendix.setVisibility(visibility);
-        }
-        mAppendixVisibility = visibility;
-    }
-
-    /**
-     * Sets the callback to be invoked when extra widget is clicked by the user.
-     *
-     * @param listener The callback to be invoked
-     */
-    public void setExtraWidgetOnClickListener(View.OnClickListener listener) {
-        mExtraWidgetOnClickListener = listener;
-
-        if (mExtraWidget == null || mExtraWidgetContainer == null) {
-            return;
-        }
-
-        mExtraWidget.setOnClickListener(mExtraWidgetOnClickListener);
-
-        mExtraWidgetContainer.setVisibility((mExtraWidgetOnClickListener != null)
-                ? View.VISIBLE : View.GONE);
-    }
-
-    private void init() {
-        setWidgetLayoutResource(R.layout.preference_widget_radiobutton);
-        setLayoutResource(R.layout.preference_radio);
-        setIconSpaceReserved(false);
-    }
-}
diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java
index 1a938d6..a4089c0 100644
--- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java
+++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java
@@ -22,6 +22,7 @@
 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_GROUP_KEY;
 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON;
 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SEARCHABLE;
 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY;
 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY_URI;
 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SWITCH_URI;
@@ -424,6 +425,13 @@
     }
 
     /**
+     * Returns if this is searchable.
+     */
+    public boolean isSearchable() {
+        return mMetaData == null || mMetaData.getBoolean(META_DATA_PREFERENCE_SEARCHABLE, true);
+    }
+
+    /**
      * The type of the tile.
      */
     public enum Type {
diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java
index 33907ec..d0929e1 100644
--- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java
+++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java
@@ -250,6 +250,11 @@
     public static final String META_DATA_NEW_TASK = "com.android.settings.new_task";
 
     /**
+     * If the entry should be shown in settings search results. Defaults to true.
+     */
+    public static final String META_DATA_PREFERENCE_SEARCHABLE = "com.android.settings.searchable";
+
+    /**
      * Build a list of DashboardCategory.
      */
     public static List<DashboardCategory> getCategories(Context context,
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ActivityTileTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ActivityTileTest.java
index 4d2b1ae..21cdc49 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ActivityTileTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ActivityTileTest.java
@@ -20,6 +20,7 @@
 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_GROUP_KEY;
 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON;
 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_URI;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SEARCHABLE;
 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SWITCH_URI;
 import static com.android.settingslib.drawer.TileUtils.PROFILE_ALL;
 import static com.android.settingslib.drawer.TileUtils.PROFILE_PRIMARY;
@@ -256,4 +257,26 @@
 
         assertThat(tile.getType()).isEqualTo(Tile.Type.SWITCH_WITH_ACTION);
     }
+
+    @Test
+    public void isSearchable_noMetadata_isTrue() {
+        final Tile tile = new ActivityTile(null, "category");
+
+        assertThat(tile.isSearchable()).isTrue();
+    }
+
+    @Test
+    public void isSearchable_notSet_isTrue() {
+        final Tile tile = new ActivityTile(mActivityInfo, "category");
+
+        assertThat(tile.isSearchable()).isTrue();
+    }
+
+    @Test
+    public void isSearchable_isSet_false() {
+        mActivityInfo.metaData.putBoolean(META_DATA_PREFERENCE_SEARCHABLE, false);
+        final Tile tile = new ActivityTile(mActivityInfo, "category");
+
+        assertThat(tile.isSearchable()).isFalse();
+    }
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ProviderTileTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ProviderTileTest.java
index 80f9efb..faccf2f 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ProviderTileTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ProviderTileTest.java
@@ -20,6 +20,7 @@
 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_GROUP_KEY;
 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON;
 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SEARCHABLE;
 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SWITCH_URI;
 import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE;
 import static com.android.settingslib.drawer.TileUtils.PROFILE_ALL;
@@ -257,6 +258,28 @@
         assertThat(tile.getType()).isEqualTo(Tile.Type.GROUP);
     }
 
+    @Test
+    public void isSearchable_noMetadata_isTrue() {
+        final Tile tile = new ProviderTile(mProviderInfo, "category", null);
+
+        assertThat(tile.isSearchable()).isTrue();
+    }
+
+    @Test
+    public void isSearchable_notSet_isTrue() {
+        final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData);
+
+        assertThat(tile.isSearchable()).isTrue();
+    }
+
+    @Test
+    public void isSearchable_isSet_false() {
+        mMetaData.putBoolean(META_DATA_PREFERENCE_SEARCHABLE, false);
+        final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData);
+
+        assertThat(tile.isSearchable()).isFalse();
+    }
+
     @Implements(TileUtils.class)
     private static class ShadowTileUtils {
 
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/RadioButtonPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/RadioButtonPreferenceTest.java
deleted file mode 100644
index 0f708cd..0000000
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/RadioButtonPreferenceTest.java
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * Copyright (C) 2019 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.settingslib.widget;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertEquals;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import android.app.Application;
-import android.view.LayoutInflater;
-import android.view.View;
-
-import androidx.preference.PreferenceViewHolder;
-
-import com.android.settingslib.widget.preference.radio.R;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-
-@RunWith(RobolectricTestRunner.class)
-public class RadioButtonPreferenceTest {
-
-    private Application mContext;
-    private RadioButtonPreference mPreference;
-
-    private View mExtraWidgetContainer;
-    private View mExtraWidget;
-
-    private boolean mIsClickListenerCalled;
-    private View.OnClickListener mClickListener = new View.OnClickListener() {
-        @Override
-        public void onClick(View v) {
-            mIsClickListenerCalled = true;
-        }
-    };
-
-    @Before
-    public void setUp() {
-        mContext = RuntimeEnvironment.application;
-        mPreference = new RadioButtonPreference(mContext);
-
-        View view = LayoutInflater.from(mContext)
-                .inflate(R.layout.preference_radio, null /* root */);
-        PreferenceViewHolder preferenceViewHolder =
-                PreferenceViewHolder.createInstanceForTests(view);
-        mPreference.onBindViewHolder(preferenceViewHolder);
-
-        mExtraWidgetContainer = view.findViewById(R.id.radio_extra_widget_container);
-        mExtraWidget = view.findViewById(R.id.radio_extra_widget);
-    }
-
-    @Test
-    public void shouldHaveRadioPreferenceLayout() {
-        assertThat(mPreference.getLayoutResource()).isEqualTo(R.layout.preference_radio);
-    }
-
-    @Test
-    public void iconSpaceReservedShouldBeFalse() {
-        assertThat(mPreference.isIconSpaceReserved()).isFalse();
-    }
-
-    @Test
-    public void onBindViewHolder_withSummary_containerShouldBeVisible() {
-        mPreference.setSummary("some summary");
-        View summaryContainer = new View(mContext);
-        View view = mock(View.class);
-        when(view.findViewById(R.id.summary_container)).thenReturn(summaryContainer);
-        PreferenceViewHolder preferenceViewHolder =
-                PreferenceViewHolder.createInstanceForTests(view);
-
-        mPreference.onBindViewHolder(preferenceViewHolder);
-
-        assertEquals(View.VISIBLE, summaryContainer.getVisibility());
-    }
-
-    @Test
-    public void onBindViewHolder_emptySummary_containerShouldBeGone() {
-        mPreference.setSummary("");
-        View summaryContainer = new View(mContext);
-        View view = mock(View.class);
-        when(view.findViewById(R.id.summary_container)).thenReturn(summaryContainer);
-        PreferenceViewHolder preferenceViewHolder =
-                PreferenceViewHolder.createInstanceForTests(view);
-
-        mPreference.onBindViewHolder(preferenceViewHolder);
-
-        assertEquals(View.GONE, summaryContainer.getVisibility());
-    }
-
-    @Test
-    public void nullSummary_containerShouldBeGone() {
-        mPreference.setSummary(null);
-        View summaryContainer = new View(mContext);
-        View view = mock(View.class);
-        when(view.findViewById(R.id.summary_container)).thenReturn(summaryContainer);
-        PreferenceViewHolder preferenceViewHolder =
-                PreferenceViewHolder.createInstanceForTests(view);
-        mPreference.onBindViewHolder(preferenceViewHolder);
-        assertEquals(View.GONE, summaryContainer.getVisibility());
-    }
-
-    @Test
-    public void setAppendixVisibility_setGone_shouldBeGone() {
-        mPreference.setAppendixVisibility(View.GONE);
-
-        View view = LayoutInflater.from(mContext)
-                .inflate(R.layout.preference_radio, null /* root */);
-        PreferenceViewHolder holder = PreferenceViewHolder.createInstanceForTests(view);
-        mPreference.onBindViewHolder(holder);
-        assertThat(holder.findViewById(R.id.appendix).getVisibility()).isEqualTo(View.GONE);
-    }
-
-    @Test
-    public void setExtraWidgetListener_setNull_extraWidgetShouldInvisible() {
-        mPreference.setExtraWidgetOnClickListener(null);
-
-        assertEquals(View.GONE, mExtraWidgetContainer.getVisibility());
-    }
-
-    @Test
-    public void setExtraWidgetListener_extraWidgetShouldVisible() {
-        mPreference.setExtraWidgetOnClickListener(mClickListener);
-
-        assertEquals(View.VISIBLE, mExtraWidgetContainer.getVisibility());
-    }
-
-    @Test
-    public void onClickListener_setExtraWidgetOnClickListener_ShouldCalled() {
-        mPreference.setExtraWidgetOnClickListener(mClickListener);
-
-        assertThat(mIsClickListenerCalled).isFalse();
-        mExtraWidget.callOnClick();
-        assertThat(mIsClickListenerCalled).isTrue();
-    }
-}
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/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING
index af6fa86..5c2f979 100644
--- a/packages/SystemUI/TEST_MAPPING
+++ b/packages/SystemUI/TEST_MAPPING
@@ -131,5 +131,22 @@
         }
       ]
     }
+  ],
+  // v2/sysui/suite/test-mapping-sysui-screenshot-test
+  "sysui-screenshot-test": [
+    {
+      "name": "SystemUIGoogleScreenshotTests",
+      "options": [
+        {
+          "exclude-annotation": "org.junit.Ignore"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        },
+        {
+          "exclude-annotation": "android.platform.test.annotations.Postsubmit"
+        }
+      ]
+    }
   ]
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
index 46a9d42..d0f2ce8 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
@@ -48,13 +48,13 @@
 import androidx.compose.ui.unit.dp
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.SceneScope
-import com.android.systemui.res.R
 import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
 import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
 import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
 import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.res.R
 import com.android.systemui.scene.shared.model.Direction
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
@@ -104,7 +104,8 @@
     modifier: Modifier = Modifier,
 ) {
     val message: BouncerViewModel.MessageViewModel by viewModel.message.collectAsState()
-    val authMethodViewModel: AuthMethodBouncerViewModel? by viewModel.authMethod.collectAsState()
+    val authMethodViewModel: AuthMethodBouncerViewModel? by
+        viewModel.authMethodViewModel.collectAsState()
     val dialogMessage: String? by viewModel.throttlingDialogMessage.collectAsState()
     var dialog: Dialog? by remember { mutableStateOf(null) }
     val backgroundColor = MaterialTheme.colorScheme.surface
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
index 8e34008..519c0a9 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
@@ -39,7 +39,9 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.TransformOrigin
 import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
 import com.android.compose.animation.scene.ElementKey
@@ -166,11 +168,18 @@
                 modifier =
                     Modifier.align(Alignment.CenterVertically)
                         // use graphicsLayer instead of Modifier.scale to anchor transform to
-                        // top left corner
+                        // the (start, top) corner
                         .graphicsLayer(
                             scaleX = 2.57f,
                             scaleY = 2.57f,
-                            transformOrigin = TransformOrigin(0f, 0.5f)
+                            transformOrigin =
+                                TransformOrigin(
+                                    when (LocalLayoutDirection.current) {
+                                        LayoutDirection.Ltr -> 0f
+                                        LayoutDirection.Rtl -> 1f
+                                    },
+                                    0.5f
+                                )
                         ),
             )
             Spacer(modifier = Modifier.weight(1f))
diff --git a/packages/SystemUI/res/drawable/volume_row_seekbar.xml b/packages/SystemUI/res/drawable/volume_row_seekbar.xml
index 7ce1ba3..d7d75d4 100644
--- a/packages/SystemUI/res/drawable/volume_row_seekbar.xml
+++ b/packages/SystemUI/res/drawable/volume_row_seekbar.xml
@@ -20,17 +20,14 @@
 <layer-list xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:paddingMode="stack" >
+    <!-- The groove used for indicating max volume !-->
     <item android:id="@android:id/background"
         android:gravity="center_vertical|fill_horizontal">
-        <inset
-            android:insetLeft="@dimen/rounded_slider_track_inset"
-            android:insetRight="@dimen/rounded_slider_track_inset" >
-            <shape>
-                <size android:height="@dimen/volume_dialog_track_width" />
-                <corners android:radius="@dimen/volume_dialog_track_corner_radius" />
-                <solid android:color="?androidprv:attr/colorAccentSecondaryVariant" />
-            </shape>
-        </inset>
+        <shape>
+            <size android:height="@dimen/volume_dialog_track_width" />
+            <corners android:radius="@dimen/volume_dialog_panel_width_half" />
+            <solid android:color="?androidprv:attr/materialColorOutlineVariant" />
+        </shape>
     </item>
     <item android:id="@android:id/progress"
         android:gravity="center_vertical|fill_horizontal">
diff --git a/packages/SystemUI/res/layout/notif_half_shelf.xml b/packages/SystemUI/res/layout/notif_half_shelf.xml
index 37b8ae0..c70f8e2 100644
--- a/packages/SystemUI/res/layout/notif_half_shelf.xml
+++ b/packages/SystemUI/res/layout/notif_half_shelf.xml
@@ -22,8 +22,7 @@
         android:layout_height="wrap_content"
         android:layout_gravity="center_horizontal|bottom"
         android:paddingStart="4dp"
-        android:paddingEnd="4dp"
->
+        android:paddingEnd="4dp">
 
     <LinearLayout
         android:id="@+id/half_shelf"
@@ -82,11 +81,21 @@
                     android:theme="@style/MainSwitch.Settingslib"/>
             </com.android.systemui.statusbar.notification.row.AppControlView>
 
-            <!-- ChannelRows get added dynamically -->
-
+            <ScrollView
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/notification_blocker_channel_list_height"
+                android:clipToPadding="false">
+                <LinearLayout
+                    android:id="@+id/scrollView"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="vertical">
+                    <!-- ChannelRows get added dynamically -->
+                </LinearLayout>
+            </ScrollView>
         </com.android.systemui.statusbar.notification.row.ChannelEditorListView>
 
-        <RelativeLayout
+        <LinearLayout
             android:id="@+id/bottom_actions"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
@@ -98,25 +107,23 @@
                 android:text="@string/see_more_title"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_alignParentStart="true"
-                android:layout_centerVertical="true"
-                android:gravity="start|center_vertical"
                 android:minWidth="@dimen/notification_importance_toggle_size"
                 android:minHeight="@dimen/notification_importance_toggle_size"
                 android:maxWidth="200dp"
                 style="@style/Widget.Dialog.Button"/>
+            <Space
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1" />
             <TextView
                 android:id="@+id/done_button"
                 android:text="@string/inline_ok_button"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_centerVertical="true"
-                android:gravity="end|center_vertical"
                 android:maxWidth="125dp"
                 android:minWidth="@dimen/notification_importance_toggle_size"
                 android:minHeight="@dimen/notification_importance_toggle_size"
-                android:layout_alignParentEnd="true"
                 style="@style/Widget.Dialog.Button"/>
-        </RelativeLayout>
+        </LinearLayout>
     </LinearLayout>
 </FrameLayout>
diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml
index 259b9ad..cfb4017 100644
--- a/packages/SystemUI/res/values-land/dimens.xml
+++ b/packages/SystemUI/res/values-land/dimens.xml
@@ -89,6 +89,9 @@
     <dimen name="global_actions_button_size">72dp</dimen>
     <dimen name="global_actions_button_padding">26dp</dimen>
 
+    <!-- scroll view the size of 2 channel rows -->
+    <dimen name="notification_blocker_channel_list_height">128dp</dimen>
+
     <dimen name="keyguard_indication_margin_bottom">8dp</dimen>
     <dimen name="lock_icon_margin_bottom">24dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 88726af..0ee5da2 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -313,6 +313,8 @@
     <!-- The space around a notification menu item  -->
     <dimen name="notification_menu_icon_padding">20dp</dimen>
 
+    <!-- scroll view the size of 3 channel rows -->
+    <dimen name="notification_blocker_channel_list_height">192dp</dimen>
     <!-- The vertical space around the buttons in the inline settings -->
     <dimen name="notification_guts_button_spacing">12dp</dimen>
 
@@ -545,7 +547,7 @@
     <!-- (volume_dialog_panel_width - rounded_slider_icon_size) / 2 -->
     <dimen name="volume_slider_icon_inset">11dp</dimen>
 
-    <dimen name="volume_dialog_track_width">4dp</dimen>
+    <dimen name="volume_dialog_track_width">40dp</dimen>
 
     <dimen name="volume_dialog_track_corner_radius">2dp</dimen>
 
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java
index 9059230..c074988 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java
@@ -282,9 +282,9 @@
         TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener);
     }
 
-    public void setRotationLockedAtAngle(int rotationSuggestion) {
+    public void setRotationLockedAtAngle(int rotationSuggestion, String caller) {
         RotationPolicy.setRotationLockAtAngle(mContext, /* enabled= */ isRotationLocked(),
-                /* rotation= */ rotationSuggestion);
+                /* rotation= */ rotationSuggestion, caller);
     }
 
     public boolean isRotationLocked() {
@@ -468,7 +468,8 @@
         if (rotationLocked || mRotationButton.isVisible()) {
             // Do not allow a change in rotation to set user rotation when docked.
             if (shouldOverrideUserLockPrefs(rotation) && rotationLocked && !mDocked) {
-                setRotationLockedAtAngle(rotation);
+                setRotationLockedAtAngle(rotation, /* caller= */
+                        "RotationButtonController#onRotationWatcherChanged");
             }
             setRotateSuggestionButtonState(false /* visible */, true /* forced */);
         }
@@ -572,7 +573,8 @@
     private void onRotateSuggestionClick(View v) {
         mUiEventLogger.log(RotationButtonEvent.ROTATION_SUGGESTION_ACCEPTED);
         incrementNumAcceptedRotationSuggestionsIfNeeded();
-        setRotationLockedAtAngle(mLastRotationSuggestion);
+        setRotationLockedAtAngle(mLastRotationSuggestion,
+                /* caller= */ "RotationButtonController#onRotateSuggestionClick");
         Log.i(TAG, "onRotateSuggestionClick() mLastRotationSuggestion=" + mLastRotationSuggestion);
         v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
     }
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/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
index 57a4224..4cfc6aa 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
@@ -185,9 +185,6 @@
     /** Whether the pattern should be visible for the currently-selected user. */
     val isPatternVisible: StateFlow<Boolean> = repository.isPatternVisible
 
-    /** The minimal length of a pattern. */
-    val minPatternLength: Int = repository.minPatternLength
-
     private var throttlingCountdownJob: Job? = null
 
     init {
@@ -243,39 +240,46 @@
      * Attempts to authenticate the user and unlock the device.
      *
      * If [tryAutoConfirm] is `true`, authentication is attempted if and only if the auth method
-     * supports auto-confirming, and the input's length is at least the code's length. Otherwise,
-     * `null` is returned.
+     * supports auto-confirming, and the input's length is at least the required length. Otherwise,
+     * `AuthenticationResult.SKIPPED` is returned.
      *
      * @param input The input from the user to try to authenticate with. This can be a list of
      *   different things, based on the current authentication method.
      * @param tryAutoConfirm `true` if called while the user inputs the code, without an explicit
      *   request to validate.
-     * @return `true` if the authentication succeeded and the device is now unlocked; `false` when
-     *   authentication failed, `null` if the check was not performed.
+     * @return The result of this authentication attempt.
      */
-    suspend fun authenticate(input: List<Any>, tryAutoConfirm: Boolean = false): Boolean? {
+    suspend fun authenticate(
+        input: List<Any>,
+        tryAutoConfirm: Boolean = false
+    ): AuthenticationResult {
         if (input.isEmpty()) {
             throw IllegalArgumentException("Input was empty!")
         }
 
+        val authMethod = getAuthenticationMethod()
         val skipCheck =
             when {
                 // We're being throttled, the UI layer should not have called this; skip the
                 // attempt.
                 isThrottled.value -> true
+                // The pattern is too short; skip the attempt.
+                authMethod == DomainLayerAuthenticationMethodModel.Pattern &&
+                    input.size < repository.minPatternLength -> true
                 // Auto-confirm attempt when the feature is not enabled; skip the attempt.
                 tryAutoConfirm && !isAutoConfirmEnabled.value -> true
                 // Auto-confirm should skip the attempt if the pin entered is too short.
-                tryAutoConfirm && input.size < repository.getPinLength() -> true
+                tryAutoConfirm &&
+                    authMethod == DomainLayerAuthenticationMethodModel.Pin &&
+                    input.size < repository.getPinLength() -> true
                 else -> false
             }
         if (skipCheck) {
-            return null
+            return AuthenticationResult.SKIPPED
         }
 
         // Attempt to authenticate:
-        val authMethod = getAuthenticationMethod()
-        val credential = authMethod.createCredential(input) ?: return null
+        val credential = authMethod.createCredential(input) ?: return AuthenticationResult.SKIPPED
         val authenticationResult = repository.checkCredential(credential)
         credential.zeroize()
 
@@ -299,7 +303,11 @@
             refreshThrottling()
         }
 
-        return authenticationResult.isSuccessful
+        return if (authenticationResult.isSuccessful) {
+            AuthenticationResult.SUCCEEDED
+        } else {
+            AuthenticationResult.FAILED
+        }
     }
 
     /** Starts refreshing the throttling state every second. */
@@ -383,3 +391,16 @@
         }
     }
 }
+
+/** Result of a user authentication attempt. */
+enum class AuthenticationResult {
+    /** Authentication succeeded and the device is now unlocked. */
+    SUCCEEDED,
+    /** Authentication failed and the device remains unlocked. */
+    FAILED,
+    /**
+     * Authentication was not performed, e.g. due to insufficient input, and the device remains
+     * unlocked.
+     */
+    SKIPPED,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 46d3c8a..79d9c1b 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -1096,8 +1096,19 @@
             // cancel the fingerprint scan.
             mCancelAodFingerUpAction = mFgExecutor.executeDelayed(this::tryAodSendFingerUp,
                     AOD_SEND_FINGER_UP_DELAY_MILLIS);
-            // using a hard-coded value for major and minor until it is available from the sensor
-            onFingerDown(requestId, screenX, screenY, minor, major);
+            // using a hard-coded value for orientation, time and gestureStart until they are
+            // available from the sensor.
+            onFingerDown(
+                    requestId,
+                    MotionEvent.INVALID_POINTER_ID /* pointerId */,
+                    screenX,
+                    screenY,
+                    minor,
+                    major,
+                    0f /* orientation */,
+                    0L /* time */,
+                    0L /* gestureStart */,
+                    true /* isAod */);
         };
 
         if (mScreenOn) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
index f2d4f89..72fcfe7 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
@@ -16,10 +16,12 @@
 
 package com.android.systemui.biometrics.dagger
 
-import com.android.systemui.biometrics.UdfpsUtils
 import android.content.res.Resources
 import com.android.internal.R
 import com.android.systemui.biometrics.EllipseOverlapDetectorParams
+import com.android.systemui.biometrics.UdfpsUtils
+import com.android.systemui.biometrics.data.repository.DisplayStateRepository
+import com.android.systemui.biometrics.data.repository.DisplayStateRepositoryImpl
 import com.android.systemui.biometrics.data.repository.FacePropertyRepository
 import com.android.systemui.biometrics.data.repository.FacePropertyRepositoryImpl
 import com.android.systemui.biometrics.data.repository.FaceSettingsRepository
@@ -28,18 +30,6 @@
 import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepositoryImpl
 import com.android.systemui.biometrics.data.repository.PromptRepository
 import com.android.systemui.biometrics.data.repository.PromptRepositoryImpl
-import com.android.systemui.biometrics.data.repository.DisplayStateRepository
-import com.android.systemui.biometrics.data.repository.DisplayStateRepositoryImpl
-import com.android.systemui.biometrics.domain.interactor.CredentialInteractor
-import com.android.systemui.biometrics.domain.interactor.CredentialInteractorImpl
-import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
-import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl
-import com.android.systemui.biometrics.domain.interactor.LogContextInteractor
-import com.android.systemui.biometrics.domain.interactor.LogContextInteractorImpl
-import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
-import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl
-import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractor
-import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractorImpl
 import com.android.systemui.biometrics.udfps.BoundingBoxOverlapDetector
 import com.android.systemui.biometrics.udfps.EllipseOverlapDetector
 import com.android.systemui.biometrics.udfps.OverlapDetector
@@ -59,9 +49,7 @@
     @SysUISingleton
     fun faceSettings(impl: FaceSettingsRepositoryImpl): FaceSettingsRepository
 
-    @Binds
-    @SysUISingleton
-    fun faceSensors(impl: FacePropertyRepositoryImpl): FacePropertyRepository
+    @Binds @SysUISingleton fun faceSensors(impl: FacePropertyRepositoryImpl): FacePropertyRepository
 
     @Binds
     @SysUISingleton
@@ -77,30 +65,6 @@
     @SysUISingleton
     fun displayStateRepository(impl: DisplayStateRepositoryImpl): DisplayStateRepository
 
-    @Binds
-    @SysUISingleton
-    fun providesPromptSelectorInteractor(
-        impl: PromptSelectorInteractorImpl
-    ): PromptSelectorInteractor
-
-    @Binds
-    @SysUISingleton
-    fun providesCredentialInteractor(impl: CredentialInteractorImpl): CredentialInteractor
-
-    @Binds
-    @SysUISingleton
-    fun providesDisplayStateInteractor(impl: DisplayStateInteractorImpl): DisplayStateInteractor
-
-    @Binds
-    @SysUISingleton
-    fun bindsLogContextInteractor(impl: LogContextInteractorImpl): LogContextInteractor
-
-    @Binds
-    @SysUISingleton
-    fun providesSideFpsOverlayInteractor(
-        impl: SideFpsOverlayInteractorImpl
-    ): SideFpsOverlayInteractor
-
     companion object {
         /** Background [Executor] for HAL related operations. */
         @Provides
@@ -110,8 +74,7 @@
         fun providesPluginExecutor(threadFactory: ThreadFactory): Executor =
             threadFactory.buildExecutorOnNewThread("biometrics")
 
-        @Provides
-        fun providesUdfpsUtils(): UdfpsUtils = UdfpsUtils()
+        @Provides fun providesUdfpsUtils(): UdfpsUtils = UdfpsUtils()
 
         @Provides
         @SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/BiometricsDomainLayerModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/BiometricsDomainLayerModule.kt
new file mode 100644
index 0000000..a590dccd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/BiometricsDomainLayerModule.kt
@@ -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 com.android.systemui.biometrics.domain
+
+import com.android.systemui.biometrics.domain.interactor.CredentialInteractor
+import com.android.systemui.biometrics.domain.interactor.CredentialInteractorImpl
+import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
+import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl
+import com.android.systemui.biometrics.domain.interactor.LogContextInteractor
+import com.android.systemui.biometrics.domain.interactor.LogContextInteractorImpl
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl
+import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractor
+import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractorImpl
+import com.android.systemui.dagger.SysUISingleton
+import dagger.Binds
+import dagger.Module
+
+@Module
+interface BiometricsDomainLayerModule {
+
+    @Binds
+    @SysUISingleton
+    fun providesPromptSelectorInteractor(
+        impl: PromptSelectorInteractorImpl
+    ): PromptSelectorInteractor
+
+    @Binds
+    @SysUISingleton
+    fun providesCredentialInteractor(impl: CredentialInteractorImpl): CredentialInteractor
+
+    @Binds
+    @SysUISingleton
+    fun providesDisplayStateInteractor(impl: DisplayStateInteractorImpl): DisplayStateInteractor
+
+    @Binds
+    @SysUISingleton
+    fun bindsLogContextInteractor(impl: LogContextInteractorImpl): LogContextInteractor
+
+    @Binds
+    @SysUISingleton
+    fun providesSideFpsOverlayInteractor(
+        impl: SideFpsOverlayInteractorImpl
+    ): SideFpsOverlayInteractor
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
index 9b2f2ba..f3a463b 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
@@ -17,8 +17,8 @@
 package com.android.systemui.bouncer.domain.interactor
 
 import android.content.Context
-import com.android.systemui.res.R
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.authentication.domain.interactor.AuthenticationResult
 import com.android.systemui.authentication.domain.model.AuthenticationMethodModel
 import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel
 import com.android.systemui.bouncer.data.repository.BouncerRepository
@@ -26,6 +26,7 @@
 import com.android.systemui.classifier.domain.interactor.FalsingInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.res.R
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.flag.SceneContainerFlags
 import com.android.systemui.scene.shared.model.SceneKey
@@ -34,6 +35,7 @@
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.async
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
@@ -92,9 +94,6 @@
     /** Whether the pattern should be visible for the currently-selected user. */
     val isPatternVisible: StateFlow<Boolean> = authenticationInteractor.isPatternVisible
 
-    /** The minimal length of a pattern. */
-    val minPatternLength = authenticationInteractor.minPatternLength
-
     init {
         if (flags.isEnabled()) {
             // Clear the message if moved from throttling to no-longer throttling.
@@ -184,33 +183,44 @@
      * dismissed and hidden.
      *
      * If [tryAutoConfirm] is `true`, authentication is attempted if and only if the auth method
-     * supports auto-confirming, and the input's length is at least the code's length. Otherwise,
-     * `null` is returned.
+     * supports auto-confirming, and the input's length is at least the required length. Otherwise,
+     * `AuthenticationResult.SKIPPED` is returned.
      *
      * @param input The input from the user to try to authenticate with. This can be a list of
      *   different things, based on the current authentication method.
      * @param tryAutoConfirm `true` if called while the user inputs the code, without an explicit
      *   request to validate.
-     * @return `true` if the authentication succeeded and the device is now unlocked; `false` when
-     *   authentication failed, `null` if the check was not performed.
+     * @return The result of this authentication attempt.
      */
     suspend fun authenticate(
         input: List<Any>,
         tryAutoConfirm: Boolean = false,
-    ): Boolean? {
-        val isAuthenticated =
-            authenticationInteractor.authenticate(input, tryAutoConfirm) ?: return null
-
-        if (isAuthenticated) {
-            sceneInteractor.changeScene(
-                scene = SceneModel(SceneKey.Gone),
-                loggingReason = "successful authentication",
-            )
-        } else {
-            showErrorMessage()
+    ): AuthenticationResult {
+        if (input.isEmpty()) {
+            return AuthenticationResult.SKIPPED
         }
-
-        return isAuthenticated
+        // Switching to the application scope here since this method is often called from
+        // view-models, whose lifecycle (and thus scope) is shorter than this interactor.
+        // This allows the task to continue running properly even when the calling scope has been
+        // cancelled.
+        return applicationScope
+            .async {
+                val authResult = authenticationInteractor.authenticate(input, tryAutoConfirm)
+                when (authResult) {
+                    // Authentication succeeded.
+                    AuthenticationResult.SUCCEEDED ->
+                        sceneInteractor.changeScene(
+                            scene = SceneModel(SceneKey.Gone),
+                            loggingReason = "successful authentication",
+                        )
+                    // Authentication failed.
+                    AuthenticationResult.FAILED -> showErrorMessage()
+                    // Authentication skipped.
+                    AuthenticationResult.SKIPPED -> if (!tryAutoConfirm) showErrorMessage()
+                }
+                authResult
+            }
+            .await()
     }
 
     /**
@@ -221,21 +231,20 @@
      * For example, if the user entered a pattern that's too short, the system can show the error
      * message without having the attempt trigger throttling.
      */
-    suspend fun showErrorMessage() {
+    private suspend fun showErrorMessage() {
         repository.setMessage(errorMessage(authenticationInteractor.getAuthenticationMethod()))
     }
 
-    /** If the bouncer is showing, hides the bouncer and return to the lockscreen scene. */
-    fun hide(
-        loggingReason: String,
-    ) {
+    /** Notifies the interactor that the input method editor has been hidden. */
+    fun onImeHidden() {
+        // If the bouncer is showing, hide it and return to the lockscreen scene.
         if (sceneInteractor.desiredScene.value.key != SceneKey.Bouncer) {
             return
         }
 
         sceneInteractor.changeScene(
             scene = SceneModel(SceneKey.Lockscreen),
-            loggingReason = loggingReason,
+            loggingReason = "IME hidden",
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
index 4546bea..66c6162 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
@@ -16,12 +16,21 @@
 
 package com.android.systemui.bouncer.ui.viewmodel
 
+import android.annotation.StringRes
+import android.util.Log
+import com.android.systemui.authentication.domain.interactor.AuthenticationResult
+import com.android.systemui.authentication.domain.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
 
 sealed class AuthMethodBouncerViewModel(
+    protected val viewModelScope: CoroutineScope,
+    protected val interactor: BouncerInteractor,
+
     /**
      * Whether user input is enabled.
      *
@@ -29,7 +38,6 @@
      * being able to attempt to unlock the device.
      */
     val isInputEnabled: StateFlow<Boolean>,
-    private val interactor: BouncerInteractor,
 ) {
 
     private val _animateFailure = MutableStateFlow(false)
@@ -42,12 +50,26 @@
     /** Whether the input method editor (for example, the software keyboard) is visible. */
     private var isImeVisible: Boolean = false
 
+    /** The authentication method that corresponds to this view model. */
+    abstract val authenticationMethod: AuthenticationMethodModel
+
     /**
-     * Notifies that the failure animation has been shown. This should be called to consume a `true`
-     * value in [animateFailure].
+     * String resource ID of the failure message to be shown during throttling.
+     *
+     * The message must include 2 number parameters: the first one indicating how many unsuccessful
+     * attempts were made, and the second one indicating in how many seconds throttling will expire.
      */
-    fun onFailureAnimationShown() {
-        _animateFailure.value = false
+    @get:StringRes abstract val throttlingMessageId: Int
+
+    /** Notifies that the UI has been shown to the user. */
+    fun onShown() {
+        clearInput()
+        interactor.resetMessage()
+    }
+
+    /** Notifies that the user has placed down a pointer. */
+    fun onDown() {
+        interactor.onDown()
     }
 
     /**
@@ -56,17 +78,44 @@
      */
     fun onImeVisibilityChanged(isVisible: Boolean) {
         if (isImeVisible && !isVisible) {
-            // The IME has gone from visible to invisible, dismiss the bouncer.
-            interactor.hide(
-                loggingReason = "IME hidden",
-            )
+            interactor.onImeHidden()
         }
 
         isImeVisible = isVisible
     }
 
-    /** Ask the UI to show the failure animation. */
-    protected fun showFailureAnimation() {
-        _animateFailure.value = true
+    /**
+     * Notifies that the failure animation has been shown. This should be called to consume a `true`
+     * value in [animateFailure].
+     */
+    fun onFailureAnimationShown() {
+        _animateFailure.value = false
+    }
+
+    /** Clears any previously-entered input. */
+    protected abstract fun clearInput()
+
+    /** Returns the input entered so far. */
+    protected abstract fun getInput(): List<Any>
+
+    /**
+     * Attempts to authenticate the user using the current input value.
+     *
+     * @see BouncerInteractor.authenticate
+     */
+    protected fun tryAuthenticate(useAutoConfirm: Boolean = false) {
+        viewModelScope.launch {
+            Log.d("Danny", "tryAuthenticate(useAutoConfirm=$useAutoConfirm)")
+            val authenticationResult = interactor.authenticate(getInput(), useAutoConfirm)
+            Log.d("Danny", "result = $authenticationResult")
+            if (authenticationResult == AuthenticationResult.SKIPPED && useAutoConfirm) {
+                return@launch
+            }
+            _animateFailure.value = authenticationResult != AuthenticationResult.SUCCEEDED
+
+            // TODO(b/291528545): On success, this should only be cleared after the view is animated
+            //  away).
+            clearInput()
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
index 15d1dae..782ead3 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
@@ -17,16 +17,19 @@
 package com.android.systemui.bouncer.ui.viewmodel
 
 import android.content.Context
-import com.android.systemui.res.R
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
 import com.android.systemui.authentication.domain.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.scene.shared.flag.SceneContainerFlags
 import javax.inject.Inject
 import kotlin.math.ceil
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
@@ -35,6 +38,7 @@
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.job
 import kotlinx.coroutines.launch
 
 /** Holds UI state and handles user input on bouncer UIs. */
@@ -44,8 +48,9 @@
 constructor(
     @Application private val applicationContext: Context,
     @Application private val applicationScope: CoroutineScope,
+    @Main private val mainDispatcher: CoroutineDispatcher,
     private val bouncerInteractor: BouncerInteractor,
-    private val authenticationInteractor: AuthenticationInteractor,
+    authenticationInteractor: AuthenticationInteractor,
     flags: SceneContainerFlags,
 ) {
     private val isInputEnabled: StateFlow<Boolean> =
@@ -57,91 +62,45 @@
                 initialValue = !bouncerInteractor.isThrottled.value,
             )
 
-    private val pin: PinBouncerViewModel by lazy {
-        PinBouncerViewModel(
-            applicationContext = applicationContext,
-            applicationScope = applicationScope,
-            interactor = bouncerInteractor,
-            isInputEnabled = isInputEnabled,
-        )
-    }
-
-    private val password: PasswordBouncerViewModel by lazy {
-        PasswordBouncerViewModel(
-            applicationScope = applicationScope,
-            interactor = bouncerInteractor,
-            isInputEnabled = isInputEnabled,
-        )
-    }
-
-    private val pattern: PatternBouncerViewModel by lazy {
-        PatternBouncerViewModel(
-            applicationContext = applicationContext,
-            applicationScope = applicationScope,
-            interactor = bouncerInteractor,
-            isInputEnabled = isInputEnabled,
-        )
-    }
-
     /** View-model for the current UI, based on the current authentication method. */
-    val authMethod: StateFlow<AuthMethodBouncerViewModel?> =
+    val authMethodViewModel: StateFlow<AuthMethodBouncerViewModel?> =
         authenticationInteractor.authenticationMethod
-            .map { authenticationMethod ->
-                when (authenticationMethod) {
-                    is AuthenticationMethodModel.Pin -> pin
-                    is AuthenticationMethodModel.Password -> password
-                    is AuthenticationMethodModel.Pattern -> pattern
-                    else -> null
-                }
-            }
+            .map(::getChildViewModel)
             .stateIn(
                 scope = applicationScope,
                 started = SharingStarted.WhileSubscribed(),
                 initialValue = null,
             )
 
+    // Handle to the scope of the child ViewModel (stored in [authMethod]).
+    private var childViewModelScope: CoroutineScope? = null
+
     init {
         if (flags.isEnabled()) {
             applicationScope.launch {
-                bouncerInteractor.isThrottled
-                    .map { isThrottled ->
-                        if (isThrottled) {
-                            when (authenticationInteractor.getAuthenticationMethod()) {
-                                is AuthenticationMethodModel.Pin ->
-                                    R.string.kg_too_many_failed_pin_attempts_dialog_message
-                                is AuthenticationMethodModel.Password ->
-                                    R.string.kg_too_many_failed_password_attempts_dialog_message
-                                is AuthenticationMethodModel.Pattern ->
-                                    R.string.kg_too_many_failed_pattern_attempts_dialog_message
-                                else -> null
-                            }?.let { stringResourceId ->
-                                applicationContext.getString(
-                                    stringResourceId,
-                                    bouncerInteractor.throttling.value.failedAttemptCount,
-                                    ceil(bouncerInteractor.throttling.value.remainingMs / 1000f)
-                                        .toInt(),
-                                )
-                            }
+                combine(bouncerInteractor.isThrottled, authMethodViewModel) {
+                        isThrottled,
+                        authMethodViewModel ->
+                        if (isThrottled && authMethodViewModel != null) {
+                            applicationContext.getString(
+                                authMethodViewModel.throttlingMessageId,
+                                bouncerInteractor.throttling.value.failedAttemptCount,
+                                ceil(bouncerInteractor.throttling.value.remainingMs / 1000f)
+                                    .toInt(),
+                            )
                         } else {
                             null
                         }
                     }
                     .distinctUntilChanged()
-                    .collect { dialogMessageOrNull ->
-                        if (dialogMessageOrNull != null) {
-                            _throttlingDialogMessage.value = dialogMessageOrNull
-                        }
-                    }
+                    .collect { dialogMessage -> _throttlingDialogMessage.value = dialogMessage }
             }
         }
     }
 
     /** The user-facing message to show in the bouncer. */
     val message: StateFlow<MessageViewModel> =
-        combine(
-                bouncerInteractor.message,
-                bouncerInteractor.isThrottled,
-            ) { message, isThrottled ->
+        combine(bouncerInteractor.message, bouncerInteractor.isThrottled) { message, isThrottled ->
                 toMessageViewModel(message, isThrottled)
             }
             .stateIn(
@@ -186,6 +145,50 @@
         )
     }
 
+    private fun getChildViewModel(
+        authenticationMethod: AuthenticationMethodModel,
+    ): AuthMethodBouncerViewModel? {
+        // If the current child view-model matches the authentication method, reuse it instead of
+        // creating a new instance.
+        val childViewModel = authMethodViewModel.value
+        if (authenticationMethod == childViewModel?.authenticationMethod) {
+            return childViewModel
+        }
+
+        childViewModelScope?.cancel()
+        val newViewModelScope = createChildCoroutineScope(applicationScope)
+        childViewModelScope = newViewModelScope
+        return when (authenticationMethod) {
+            is AuthenticationMethodModel.Pin ->
+                PinBouncerViewModel(
+                    applicationContext = applicationContext,
+                    viewModelScope = newViewModelScope,
+                    interactor = bouncerInteractor,
+                    isInputEnabled = isInputEnabled,
+                )
+            is AuthenticationMethodModel.Password ->
+                PasswordBouncerViewModel(
+                    viewModelScope = newViewModelScope,
+                    interactor = bouncerInteractor,
+                    isInputEnabled = isInputEnabled,
+                )
+            is AuthenticationMethodModel.Pattern ->
+                PatternBouncerViewModel(
+                    applicationContext = applicationContext,
+                    viewModelScope = newViewModelScope,
+                    interactor = bouncerInteractor,
+                    isInputEnabled = isInputEnabled,
+                )
+            else -> null
+        }
+    }
+
+    private fun createChildCoroutineScope(parentScope: CoroutineScope): CoroutineScope {
+        return CoroutineScope(
+            SupervisorJob(parent = parentScope.coroutineContext.job) + mainDispatcher
+        )
+    }
+
     data class MessageViewModel(
         val text: String,
 
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
index 9e10f29..fe77419 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
@@ -16,22 +16,24 @@
 
 package com.android.systemui.bouncer.ui.viewmodel
 
+import com.android.systemui.authentication.domain.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.res.R
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.launch
 
 /** Holds UI state and handles user input for the password bouncer UI. */
 class PasswordBouncerViewModel(
-    private val applicationScope: CoroutineScope,
-    private val interactor: BouncerInteractor,
+    viewModelScope: CoroutineScope,
+    interactor: BouncerInteractor,
     isInputEnabled: StateFlow<Boolean>,
 ) :
     AuthMethodBouncerViewModel(
-        isInputEnabled = isInputEnabled,
+        viewModelScope = viewModelScope,
         interactor = interactor,
+        isInputEnabled = isInputEnabled,
     ) {
 
     private val _password = MutableStateFlow("")
@@ -39,10 +41,16 @@
     /** The password entered so far. */
     val password: StateFlow<String> = _password.asStateFlow()
 
-    /** Notifies that the UI has been shown to the user. */
-    fun onShown() {
+    override val authenticationMethod = AuthenticationMethodModel.Password
+
+    override val throttlingMessageId = R.string.kg_too_many_failed_password_attempts_dialog_message
+
+    override fun clearInput() {
         _password.value = ""
-        interactor.resetMessage()
+    }
+
+    override fun getInput(): List<Any> {
+        return _password.value.toCharArray().toList()
     }
 
     /** Notifies that the user has changed the password input. */
@@ -60,17 +68,8 @@
 
     /** Notifies that the user has pressed the key for attempting to authenticate the password. */
     fun onAuthenticateKeyPressed() {
-        val password = _password.value.toCharArray().toList()
-        if (password.isEmpty()) {
-            return
-        }
-
-        _password.value = ""
-
-        applicationScope.launch {
-            if (interactor.authenticate(password) != true) {
-                showFailureAnimation()
-            }
+        if (_password.value.isNotEmpty()) {
+            tryAuthenticate()
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
index 497276b..52adf54 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
@@ -18,8 +18,10 @@
 
 import android.content.Context
 import android.util.TypedValue
+import com.android.systemui.authentication.domain.model.AuthenticationMethodModel
 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.res.R
 import kotlin.math.max
 import kotlin.math.min
 import kotlin.math.pow
@@ -31,18 +33,18 @@
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.launch
 
 /** Holds UI state and handles user input for the pattern bouncer UI. */
 class PatternBouncerViewModel(
     private val applicationContext: Context,
-    private val applicationScope: CoroutineScope,
-    private val interactor: BouncerInteractor,
+    viewModelScope: CoroutineScope,
+    interactor: BouncerInteractor,
     isInputEnabled: StateFlow<Boolean>,
 ) :
     AuthMethodBouncerViewModel(
-        isInputEnabled = isInputEnabled,
+        viewModelScope = viewModelScope,
         interactor = interactor,
+        isInputEnabled = isInputEnabled,
     ) {
 
     /** The number of columns in the dot grid. */
@@ -58,7 +60,7 @@
         _selectedDots
             .map { it.toList() }
             .stateIn(
-                scope = applicationScope,
+                scope = viewModelScope,
                 started = SharingStarted.WhileSubscribed(),
                 initialValue = emptyList(),
             )
@@ -76,15 +78,9 @@
     /** Whether the pattern itself should be rendered visibly. */
     val isPatternVisible: StateFlow<Boolean> = interactor.isPatternVisible
 
-    /** Notifies that the UI has been shown to the user. */
-    fun onShown() {
-        interactor.resetMessage()
-    }
+    override val authenticationMethod = AuthenticationMethodModel.Pattern
 
-    /** Notifies that the user has placed down a pointer, not necessarily dragging just yet. */
-    fun onDown() {
-        interactor.onDown()
-    }
+    override val throttlingMessageId = R.string.kg_too_many_failed_pattern_attempts_dialog_message
 
     /** Notifies that the user has started a drag gesture across the dot grid. */
     fun onDragStart() {
@@ -164,24 +160,23 @@
 
     /** Notifies that the user has ended the drag gesture across the dot grid. */
     fun onDragEnd() {
-        val pattern = _selectedDots.value.map { it.toCoordinate() }
-
+        val pattern = getInput()
         if (pattern.size == 1) {
             // Single dot patterns are treated as erroneous/false taps:
             interactor.onFalseUserInput()
         }
 
+        tryAuthenticate()
+    }
+
+    override fun clearInput() {
         _dots.value = defaultDots()
         _currentDot.value = null
         _selectedDots.value = linkedSetOf()
+    }
 
-        applicationScope.launch {
-            if (pattern.size < interactor.minPatternLength) {
-                interactor.showErrorMessage()
-            } else if (interactor.authenticate(pattern) != true) {
-                showFailureAnimation()
-            }
-        }
+    override fun getInput(): List<Any> {
+        return _selectedDots.value.map(PatternDotViewModel::toCoordinate)
     }
 
     private fun defaultDots(): List<PatternDotViewModel> {
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
index 8e6421e..b90e255 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
@@ -18,7 +18,9 @@
 
 import android.content.Context
 import com.android.keyguard.PinShapeAdapter
+import com.android.systemui.authentication.domain.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.res.R
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
@@ -26,18 +28,18 @@
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.launch
 
 /** Holds UI state and handles user input for the PIN code bouncer UI. */
 class PinBouncerViewModel(
     applicationContext: Context,
-    private val applicationScope: CoroutineScope,
-    private val interactor: BouncerInteractor,
+    viewModelScope: CoroutineScope,
+    interactor: BouncerInteractor,
     isInputEnabled: StateFlow<Boolean>,
 ) :
     AuthMethodBouncerViewModel(
-        isInputEnabled = isInputEnabled,
+        viewModelScope = viewModelScope,
         interactor = interactor,
+        isInputEnabled = isInputEnabled,
     ) {
 
     val pinShapes = PinShapeAdapter(applicationContext)
@@ -61,7 +63,7 @@
                 )
             }
             .stateIn(
-                scope = applicationScope,
+                scope = viewModelScope,
                 // Make sure this is kept as WhileSubscribed or we can run into a bug where the
                 // downstream continues to receive old/stale/cached values.
                 started = SharingStarted.WhileSubscribed(),
@@ -73,21 +75,14 @@
         interactor.isAutoConfirmEnabled
             .map { if (it) ActionButtonAppearance.Hidden else ActionButtonAppearance.Shown }
             .stateIn(
-                scope = applicationScope,
+                scope = viewModelScope,
                 started = SharingStarted.Eagerly,
                 initialValue = ActionButtonAppearance.Hidden,
             )
 
-    /** Notifies that the UI has been shown to the user. */
-    fun onShown() {
-        clearPinInput()
-        interactor.resetMessage()
-    }
+    override val authenticationMethod = AuthenticationMethodModel.Pin
 
-    /** Notifies that the user has placed down a pointer. */
-    fun onDown() {
-        interactor.onDown()
-    }
+    override val throttlingMessageId = R.string.kg_too_many_failed_pin_attempts_dialog_message
 
     /** Notifies that the user clicked on a PIN button with the given digit value. */
     fun onPinButtonClicked(input: Int) {
@@ -109,7 +104,8 @@
 
     /** Notifies that the user long-pressed the backspace button. */
     fun onBackspaceButtonLongPressed() {
-        clearPinInput()
+        clearInput()
+        interactor.clearMessage()
     }
 
     /** Notifies that the user clicked the "enter" button. */
@@ -117,24 +113,12 @@
         tryAuthenticate(useAutoConfirm = false)
     }
 
-    private fun clearPinInput() {
+    override fun clearInput() {
         mutablePinInput.value = mutablePinInput.value.clearAll()
     }
 
-    private fun tryAuthenticate(useAutoConfirm: Boolean) {
-        val pinCode = mutablePinInput.value.getPin()
-
-        applicationScope.launch {
-            val isSuccess = interactor.authenticate(pinCode, useAutoConfirm) ?: return@launch
-
-            if (!isSuccess) {
-                showFailureAnimation()
-            }
-
-            // TODO(b/291528545): this should not be cleared on success (at least until the view
-            // is animated away).
-            clearPinInput()
-        }
+    override fun getInput(): List<Any> {
+        return mutablePinInput.value.getPin()
     }
 
     private fun computeBackspaceButtonAppearance(
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index a325ee2..283a07b 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -38,6 +38,7 @@
 import com.android.systemui.biometrics.FingerprintReEnrollNotification;
 import com.android.systemui.biometrics.UdfpsDisplayModeProvider;
 import com.android.systemui.biometrics.dagger.BiometricsModule;
+import com.android.systemui.biometrics.domain.BiometricsDomainLayerModule;
 import com.android.systemui.bouncer.ui.BouncerViewModule;
 import com.android.systemui.classifier.FalsingModule;
 import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule;
@@ -122,6 +123,7 @@
 import com.android.systemui.tuner.dagger.TunerModule;
 import com.android.systemui.unfold.SysUIUnfoldModule;
 import com.android.systemui.user.UserModule;
+import com.android.systemui.user.domain.UserDomainLayerModule;
 import com.android.systemui.util.concurrency.SysUIConcurrencyModule;
 import com.android.systemui.util.dagger.UtilModule;
 import com.android.systemui.util.kotlin.CoroutinesModule;
@@ -162,6 +164,7 @@
         AssistModule.class,
         AuthenticationModule.class,
         BiometricsModule.class,
+        BiometricsDomainLayerModule.class,
         BouncerViewModule.class,
         ClipboardOverlayModule.class,
         ClockRegistryModule.class,
@@ -209,6 +212,7 @@
         TelephonyRepositoryModule.class,
         TemporaryDisplayModule.class,
         TunerModule.class,
+        UserDomainLayerModule.class,
         UserModule.class,
         UtilModule.class,
         NoteTaskModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index ecad9d7..3b70555 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -253,7 +253,8 @@
 
     /** Provide new auth messages on the bouncer. */
     // TODO(b/277961132): Tracking bug.
-    @JvmField val REVAMPED_BOUNCER_MESSAGES = unreleasedFlag("revamped_bouncer_messages")
+    @JvmField val REVAMPED_BOUNCER_MESSAGES = unreleasedFlag("revamped_bouncer_messages",
+            teamfood = true)
 
     /** Keyguard Migration */
 
@@ -297,6 +298,11 @@
     @JvmField val MIGRATE_KEYGUARD_STATUS_BAR_VIEW =
         unreleasedFlag("migrate_keyguard_status_bar_view")
 
+    /** Migrate clocks from keyguard status view to keyguard root view*/
+    // TODO(b/301502635): Tracking Bug.
+    @JvmField val MIGRATE_CLOCKS_TO_BLUEPRINT =
+            unreleasedFlag("migrate_clocks_to_blueprint")
+
     /** Enables preview loading animation in the wallpaper picker. */
     // TODO(b/274443705): Tracking Bug
     @JvmField
@@ -762,6 +768,9 @@
     // TODO(b/289573946): Tracking Bug
     @JvmField val PRECOMPUTED_TEXT = unreleasedFlag("precomputed_text", teamfood = true)
 
+    // TODO(b/302087895): Tracking Bug
+    @JvmField val CALL_LAYOUT_ASYNC_SET_DATA = unreleasedFlag("call_layout_async_set_data")
+
     // 2900 - CentralSurfaces-related flags
 
     // TODO(b/285174336): Tracking Bug
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/media/controls/ui/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt
index e59fc15..a48e56a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt
@@ -606,6 +606,9 @@
                 }
                     ?: mediaCarouselScrollHandler.scrollToPlayer(destIndex = mediaIndex)
             }
+        } else if (isRtl && mediaContent.childCount > 0) {
+            // In RTL, Scroll to the first player as it is the rightmost player in media carousel.
+            mediaCarouselScrollHandler.scrollToPlayer(destIndex = 0)
         }
         // Check postcondition: mediaContent should have the same number of children as there
         // are
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt
index 1261152..02f0d12 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt
@@ -282,13 +282,14 @@
             // It's an up and the fling didn't take it above
             val relativePos = scrollView.relativeScrollX % playerWidthPlusPadding
             val scrollXAmount: Int =
-                if (isRtl xor (relativePos > playerWidthPlusPadding / 2)) {
+                if (relativePos > playerWidthPlusPadding / 2) {
                     playerWidthPlusPadding - relativePos
                 } else {
                     -1 * relativePos
                 }
             if (scrollXAmount != 0) {
-                val newScrollX = scrollView.relativeScrollX + scrollXAmount
+                val dx = if (isRtl) -scrollXAmount else scrollXAmount
+                val newScrollX = scrollView.scrollX + dx
                 // Delay the scrolling since scrollView calls springback which cancels
                 // the animation again..
                 mainExecutor.execute { scrollView.smoothScrollTo(newScrollX, scrollView.scrollY) }
@@ -539,7 +540,8 @@
         // If the removed media item is "left of" the active one (in an absolute sense), we need to
         // scroll the view to keep that player in view.  This is because scroll position is always
         // calculated from left to right.
-        val leftOfActive = if (isRtl) !beforeActive else beforeActive
+        // For RTL, we need to scroll if the visible media player is the last item.
+        val leftOfActive = if (isRtl && visibleMediaIndex != 0) !beforeActive else beforeActive
         if (leftOfActive) {
             scrollView.scrollX = Math.max(scrollView.scrollX - playerWidthPlusPadding, 0)
         }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaScrollView.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaScrollView.kt
index 0e07465..10512f1 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaScrollView.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaScrollView.kt
@@ -74,6 +74,14 @@
             scrollX = transformScrollX(value)
         }
 
+    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
+        if (!isLaidOut && isLayoutRtl) {
+            // Reset scroll because onLayout method overrides RTL scroll if view was not laid out.
+            mScrollX = relativeScrollX
+        }
+        super.onLayout(changed, l, t, r, b)
+    }
+
     /** Allow all scrolls to go through, use base implementation */
     override fun scrollTo(x: Int, y: Int) {
         if (mScrollX != x || mScrollY != y) {
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index cb52a5f..ad1c77d 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -795,7 +795,8 @@
             // Reset user rotation pref to match that of the WindowManager if starting in locked
             // mode. This will automatically happen when switching from auto-rotate to locked mode.
             if (display != null && rotationButtonController.isRotationLocked()) {
-                rotationButtonController.setRotationLockedAtAngle(display.getRotation());
+                rotationButtonController.setRotationLockedAtAngle(
+                        display.getRotation(), /* caller= */ "NavigationBar#onViewAttached");
             }
         } else {
             mDisabledFlags2 |= StatusBarManager.DISABLE2_ROTATE_SUGGESTIONS;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
index 826f75f..19012e2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
@@ -78,6 +78,14 @@
     private int mMinRows = 1;
     private int mMaxColumns = TileLayout.NO_MAX_COLUMNS;
 
+    /**
+     * it's fine to read this value when class is initialized because SysUI is always restarted
+     * when running tests in test harness, see SysUiTestIsolationRule. This check is done quite
+     * often - with every shade open action - so we don't want to potentially make it less
+     * performant only for test use case
+     */
+    private boolean mRunningInTestHarness = ActivityManager.isRunningInTestHarness();
+
     public PagedTileLayout(Context context, AttributeSet attrs) {
         super(context, attrs);
         mScroller = new Scroller(context, SCROLL_CUBIC);
@@ -590,11 +598,11 @@
     private boolean shouldNotRunAnimation(Set<String> tilesToReveal) {
         boolean noAnimationNeeded = tilesToReveal.isEmpty() || mPages.size() < 2;
         boolean scrollingInProgress = getScrollX() != 0 || !beginFakeDrag();
-        // isRunningInTestHarness() to disable animation in functional testing as it caused
+        // checking mRunningInTestHarness to disable animation in functional testing as it caused
         // flakiness and is not needed there. Alternative solutions were more complex and would
         // still be either potentially flaky or modify internal data.
         // For more info see b/253493927 and b/293234595
-        return noAnimationNeeded || scrollingInProgress || ActivityManager.isRunningInTestHarness();
+        return noAnimationNeeded || scrollingInProgress || mRunningInTestHarness;
     }
 
     private int sanitizePageAction(int action) {
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/qs/tiles/RotationLockTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java
index ea162fa..5a95004 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java
@@ -133,7 +133,7 @@
     @Override
     protected void handleClick(@Nullable View view) {
         final boolean newState = !mState.value;
-        mController.setRotationLocked(!newState);
+        mController.setRotationLocked(!newState, /* caller= */ "RotationLockTile#handleClick");
         refreshState(newState);
     }
 
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/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
index e632214..37a4ef1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
@@ -104,8 +104,9 @@
     // Record the HISTORY_SIZE most recent states
     private int mHistoryIndex = 0;
     private HistoricalState[] mHistoricalRecords = new HistoricalState[HISTORY_SIZE];
-    // This is used by InteractionJankMonitor to get callback from HWUI.
+    // These views are used by InteractionJankMonitor to get callback from HWUI.
     private View mView;
+    private KeyguardClockSwitch mClockSwitchView;
 
     /**
      * If any of the system bars is hidden.
@@ -334,6 +335,7 @@
         if ((mView == null || !mView.isAttachedToWindow())
                 && (view != null && view.isAttachedToWindow())) {
             mView = view;
+            mClockSwitchView = view.findViewById(R.id.keyguard_clock_container);
         }
         mDozeAmountTarget = dozeAmount;
         if (animated) {
@@ -416,21 +418,12 @@
 
     /** Returns the id of the currently rendering clock */
     public String getClockId() {
-        if (mView == null) {
-            return KeyguardClockSwitch.MISSING_CLOCK_ID;
-        }
-
-        View clockSwitch = mView.findViewById(R.id.keyguard_clock_container);
-        if (clockSwitch == null) {
+        if (mClockSwitchView == null) {
             Log.e(TAG, "Clock container was missing");
             return KeyguardClockSwitch.MISSING_CLOCK_ID;
         }
-        if (!(clockSwitch instanceof KeyguardClockSwitch)) {
-            Log.e(TAG, "Clock container was incorrect type: " + clockSwitch);
-            return KeyguardClockSwitch.MISSING_CLOCK_ID;
-        }
 
-        return ((KeyguardClockSwitch) clockSwitch).getClockId();
+        return mClockSwitchView.getClockId();
     }
 
     private void beginInteractionJankMonitor() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
index eb31bd3..2d5afd5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
@@ -50,7 +50,7 @@
      * Set of summary keys whose groups are expanded.
      * NOTE: This should not be modified without notifying listeners, so prefer using
      * {@code setGroupExpanded} when making changes.
-      */
+     */
     private final Set<NotificationEntry> mExpandedGroups = new HashSet<>();
 
     private final FeatureFlags mFeatureFlags;
@@ -104,7 +104,18 @@
 
     @Override
     public void setGroupExpanded(NotificationEntry entry, boolean expanded) {
-        final NotificationEntry groupSummary = mGroupMembershipManager.getGroupSummary(entry);
+        NotificationEntry groupSummary = mGroupMembershipManager.getGroupSummary(entry);
+        if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE)
+                && entry.getParent() == null) {
+            if (expanded) {
+                throw new IllegalArgumentException("Cannot expand group that is not attached");
+            } else {
+                // The entry is no longer attached, but we still want to make sure we don't have
+                // a stale expansion state.
+                groupSummary = entry;
+            }
+        }
+
         boolean changed;
         if (expanded) {
             changed = mExpandedGroups.add(groupSummary);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java
index c33e8ab..3158782 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java
@@ -25,18 +25,18 @@
 import java.util.List;
 
 /**
- * Helper that determines the group states (parent, summary, children) of a notification.
+ * Helper that determines the group states (parent, summary, children) of a notification. This
+ * generally assumes that the notification is attached (aka its parent is not null).
  */
 public interface GroupMembershipManager {
     /**
-     * @return whether a given notification is a top level entry or is the summary in a group which
-     * has children
+     * @return whether a given notification is the summary in a group which has children
      */
     boolean isGroupSummary(@NonNull NotificationEntry entry);
 
     /**
      * Get the summary of a specified status bar notification. For an isolated notification this
-     * returns itself.
+     * returns null, but if called directly on a summary it returns itself.
      */
     @Nullable
     NotificationEntry getGroupSummary(@NonNull NotificationEntry entry);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java
index a6b855f..cb79353 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java
@@ -22,7 +22,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.FeatureFlagsClassic;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.statusbar.notification.collection.GroupEntry;
 import com.android.systemui.statusbar.notification.collection.ListEntry;
@@ -38,47 +38,50 @@
  */
 @SysUISingleton
 public class GroupMembershipManagerImpl implements GroupMembershipManager {
-    FeatureFlags mFeatureFlags;
+    FeatureFlagsClassic mFeatureFlags;
 
     @Inject
-    public GroupMembershipManagerImpl(FeatureFlags featureFlags) {
+    public GroupMembershipManagerImpl(FeatureFlagsClassic featureFlags) {
         mFeatureFlags = featureFlags;
     }
 
     @Override
     public boolean isGroupSummary(@NonNull NotificationEntry entry) {
-        return getGroupSummary(entry) == entry;
+        if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE)) {
+            if (entry.getParent() == null) {
+                // The entry is not attached, so it doesn't count.
+                return false;
+            }
+            // If entry is a summary, its parent is a GroupEntry with summary = entry.
+            return entry.getParent().getSummary() == entry;
+        } else {
+            return getGroupSummary(entry) == entry;
+        }
     }
 
     @Nullable
     @Override
     public NotificationEntry getGroupSummary(@NonNull NotificationEntry entry) {
-        if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE)) {
-            if (!isChildInGroup(entry)) {
-                return entry.getRepresentativeEntry();
-            }
-        } else {
-            if (isEntryTopLevel(entry) || entry.getParent() == null) {
-                return null;
-            }
+        if (isTopLevelEntry(entry) || entry.getParent() == null) {
+            return null;
         }
-
-        return entry.getParent().getRepresentativeEntry();
+        return entry.getParent().getSummary();
     }
 
     @Override
     public boolean isChildInGroup(@NonNull NotificationEntry entry) {
         if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE)) {
-            return !isEntryTopLevel(entry) && entry.getParent() != null;
+            // An entry is a child if it's not a summary or top level entry, but it is attached.
+            return !isGroupSummary(entry) && !isTopLevelEntry(entry) && entry.getParent() != null;
         } else {
-            return !isEntryTopLevel(entry);
+            return !isTopLevelEntry(entry);
         }
     }
 
     @Override
     public boolean isOnlyChildInGroup(@NonNull NotificationEntry entry) {
         if (entry.getParent() == null) {
-            return false;
+            return false; // The entry is not attached.
         }
 
         return !isGroupSummary(entry) && entry.getParent().getChildren().size() == 1;
@@ -103,7 +106,7 @@
         return null;
     }
 
-    private boolean isEntryTopLevel(@NonNull NotificationEntry entry) {
+    private boolean isTopLevelEntry(@NonNull NotificationEntry entry) {
         return entry.getParent() == ROOT_ENTRY;
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java
index 88994b9..6ec9dbe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java
@@ -100,7 +100,11 @@
         /**
          * The notification is coming from a suspended packages, so FSI is suppressed.
          */
-        NO_FSI_SUSPENDED(false);
+        NO_FSI_SUSPENDED(false),
+        /**
+         * The device is not provisioned, launch FSI.
+         */
+        FSI_NOT_PROVISIONED(true);
 
         public final boolean shouldLaunch;
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
index 0c43da0..3819843 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
@@ -45,6 +45,7 @@
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
@@ -75,6 +76,7 @@
     private final KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider;
     private final UiEventLogger mUiEventLogger;
     private final UserTracker mUserTracker;
+    private final DeviceProvisionedController mDeviceProvisionedController;
 
     @VisibleForTesting
     protected boolean mUseHeadsUp = false;
@@ -121,7 +123,8 @@
             NotifPipelineFlags flags,
             KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
             UiEventLogger uiEventLogger,
-            UserTracker userTracker) {
+            UserTracker userTracker,
+            DeviceProvisionedController deviceProvisionedController) {
         mContentResolver = contentResolver;
         mPowerManager = powerManager;
         mBatteryController = batteryController;
@@ -163,6 +166,7 @@
                     headsUpObserver);
         }
         headsUpObserver.onChange(true); // set up
+        mDeviceProvisionedController = deviceProvisionedController;
     }
 
     @Override
@@ -334,6 +338,12 @@
             }
         }
 
+        // The device is not provisioned, launch FSI.
+        if (!mDeviceProvisionedController.isDeviceProvisioned()) {
+            return getDecisionGivenSuppression(FullScreenIntentDecision.FSI_NOT_PROVISIONED,
+                    suppressedByDND);
+        }
+
         // Detect the case determined by b/231322873 to launch FSI while device is in use,
         // as blocked by the correct implementation, and report the event.
         return getDecisionGivenSuppression(FullScreenIntentDecision.NO_FSI_NO_HUN_OR_KEYGUARD,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt
index 4114eb2..a8d59d8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt
@@ -71,15 +71,15 @@
     private var appUid: Int? = null
     private var packageName: String? = null
     private var appName: String? = null
+    private var channel: NotificationChannel? = null
     private var onSettingsClickListener: NotificationInfo.OnSettingsClickListener? = null
 
     // Caller should set this if they care about when we dismiss
     var onFinishListener: OnChannelEditorDialogFinishedListener? = null
 
-    @VisibleForTesting
-    internal val paddedChannels = mutableListOf<NotificationChannel>()
     // Channels handed to us from NotificationInfo
-    private val providedChannels = mutableListOf<NotificationChannel>()
+    @VisibleForTesting
+    internal val channelList = mutableListOf<NotificationChannel>()
 
     // Map from NotificationChannel to importance
     private val edits = mutableMapOf<NotificationChannel, Int>()
@@ -93,14 +93,14 @@
     private val channelGroupList = mutableListOf<NotificationChannelGroup>()
 
     /**
-     * Give the controller all of the information it needs to present the dialog
+     * Give the controller all the information it needs to present the dialog
      * for a given app. Does a bunch of querying of NoMan, but won't present anything yet
      */
     fun prepareDialogForApp(
         appName: String,
         packageName: String,
         uid: Int,
-        channels: Set<NotificationChannel>,
+        channel: NotificationChannel,
         appIcon: Drawable,
         onSettingsClickListener: NotificationInfo.OnSettingsClickListener?
     ) {
@@ -110,6 +110,7 @@
         this.appIcon = appIcon
         this.appNotificationsEnabled = checkAreAppNotificationsOn()
         this.onSettingsClickListener = onSettingsClickListener
+        this.channel = channel
 
         // These will always start out the same
         appNotificationsCurrentlyEnabled = appNotificationsEnabled
@@ -117,9 +118,7 @@
         channelGroupList.clear()
         channelGroupList.addAll(fetchNotificationChannelGroups())
         buildGroupNameLookup()
-        providedChannels.clear()
-        providedChannels.addAll(channels)
-        padToFourChannels(channels)
+        populateChannelList()
         initDialog()
 
         prepared = true
@@ -133,36 +132,26 @@
         }
     }
 
-    private fun padToFourChannels(channels: Set<NotificationChannel>) {
-        paddedChannels.clear()
-        // First, add all of the given channels
-        paddedChannels.addAll(channels.asSequence().take(4))
-
-        // Then pad to 4 if we haven't been given that many
-        paddedChannels.addAll(getDisplayableChannels(channelGroupList.asSequence())
-                .filterNot { paddedChannels.contains(it) }
-                .distinct()
-                .take(4 - paddedChannels.size))
-
-        // If we only got one channel and it has the default miscellaneous tag, then we actually
-        // are looking at an app with a targetSdk <= O, and it doesn't make much sense to show the
-        // channel
-        if (paddedChannels.size == 1 && DEFAULT_CHANNEL_ID == paddedChannels[0].id) {
-            paddedChannels.clear()
+    private fun populateChannelList() {
+        channelList.clear()
+        if (DEFAULT_CHANNEL_ID != channel!!.id) {
+            channelList.add(0, channel!!)
+            channelList.addAll(getDisplayableChannels(channelGroupList.asSequence())
+                    .filterNot { it.id == channel!!.id }
+                    .distinct())
         }
     }
 
     private fun getDisplayableChannels(
         groupList: Sequence<NotificationChannelGroup>
     ): Sequence<NotificationChannel> {
-
-        // TODO (b/194833441): remove channel level settings when we move to a permission
         val channels = groupList
                 .flatMap { group ->
-                    group.channels.asSequence().filterNot { channel ->
-                        channel.importance == IMPORTANCE_NONE ||
+                    group.channels.asSequence()
+                            .sortedWith(compareBy {group.name?.toString() ?: group.id})
+                            .filterNot { channel ->
                                 channel.isImportanceLockedByCriticalDeviceFunction
-                    }
+                            }
                 }
 
         // TODO: sort these by avgSentWeekly, but for now let's just do alphabetical (why not)
@@ -196,8 +185,7 @@
         appNotificationsCurrentlyEnabled = null
 
         edits.clear()
-        paddedChannels.clear()
-        providedChannels.clear()
+        channelList.clear()
         groupNameLookup.clear()
     }
 
@@ -231,7 +219,7 @@
     @Suppress("unchecked_cast")
     private fun fetchNotificationChannelGroups(): List<NotificationChannelGroup> {
         return try {
-            noMan.getNotificationChannelGroupsForPackage(packageName!!, appUid!!, false)
+            noMan.getRecentBlockedNotificationChannelGroupsForPackage(packageName!!, appUid!!)
                     .list as? List<NotificationChannelGroup> ?: listOf()
         } catch (e: Exception) {
             Log.e(TAG, "Error fetching channel groups", e)
@@ -280,7 +268,6 @@
 
     @VisibleForTesting
     fun launchSettings(sender: View) {
-        val channel = if (providedChannels.size == 1) providedChannels[0] else null
         onSettingsClickListener?.onClick(sender, channel, appUid!!)
     }
 
@@ -301,14 +288,12 @@
                 controller = this@ChannelEditorDialogController
                 appIcon = this@ChannelEditorDialogController.appIcon
                 appName = this@ChannelEditorDialogController.appName
-                channels = paddedChannels
+                channels = channelList
             }
 
             setOnShowListener {
-                // play a highlight animation for the given channels
-                for (channel in providedChannels) {
-                    listView?.highlightChannel(channel)
-                }
+                // play a highlight animation for the given channel
+                listView?.highlightChannel(channel!!)
             }
 
             findViewById<TextView>(R.id.done_button)?.setOnClickListener {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt
index 2cfd075..10e67a4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt
@@ -20,6 +20,7 @@
 import android.animation.ValueAnimator
 import android.app.NotificationChannel
 import android.app.NotificationManager.IMPORTANCE_DEFAULT
+import android.app.NotificationManager.IMPORTANCE_LOW
 import android.app.NotificationManager.IMPORTANCE_NONE
 import android.app.NotificationManager.IMPORTANCE_UNSPECIFIED
 import android.content.Context
@@ -55,12 +56,14 @@
 
     // The first row is for the entire app
     private lateinit var appControlRow: AppControlView
+    private lateinit var channelListView: LinearLayout
     private val channelRows = mutableListOf<ChannelRow>()
 
     override fun onFinishInflate() {
         super.onFinishInflate()
 
         appControlRow = requireViewById(R.id.app_control)
+        channelListView = requireViewById(R.id.scrollView)
     }
 
     /**
@@ -102,7 +105,7 @@
 
         // Remove any rows
         for (row in channelRows) {
-            removeView(row)
+            channelListView.removeView(row)
         }
         channelRows.clear()
 
@@ -122,7 +125,7 @@
         row.channel = channel
 
         channelRows.add(row)
-        addView(row)
+        channelListView.addView(row)
     }
 
     private fun updateAppControlRow(enabled: Boolean) {
@@ -179,7 +182,9 @@
         switch = requireViewById(R.id.toggle)
         switch.setOnCheckedChangeListener { _, b ->
             channel?.let {
-                controller.proposeEditForChannel(it, if (b) it.importance else IMPORTANCE_NONE)
+                controller.proposeEditForChannel(it,
+                        if (b) it.originalImportance.coerceAtLeast(IMPORTANCE_LOW)
+                        else IMPORTANCE_NONE)
             }
         }
         setOnClickListener { switch.toggle() }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index fb8024c..d18f991 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -2658,42 +2658,6 @@
     }
 
     /**
-     * Returns the number of channels covered by the notification row (including its children if
-     * it's a summary notification).
-     */
-    public int getNumUniqueChannels() {
-        return getUniqueChannels().size();
-    }
-
-    /**
-     * Returns the channels covered by the notification row (including its children if
-     * it's a summary notification).
-     */
-    public ArraySet<NotificationChannel> getUniqueChannels() {
-        ArraySet<NotificationChannel> channels = new ArraySet<>();
-
-        channels.add(mEntry.getChannel());
-
-        // If this is a summary, then add in the children notification channels for the
-        // same user and pkg.
-        if (mIsSummaryWithChildren) {
-            final List<ExpandableNotificationRow> childrenRows = getAttachedChildren();
-            final int numChildren = childrenRows.size();
-            for (int i = 0; i < numChildren; i++) {
-                final ExpandableNotificationRow childRow = childrenRows.get(i);
-                final NotificationChannel childChannel = childRow.getEntry().getChannel();
-                final StatusBarNotification childSbn = childRow.getEntry().getSbn();
-                if (childSbn.getUser().equals(mEntry.getSbn().getUser())
-                        && childSbn.getPackageName().equals(mEntry.getSbn().getPackageName())) {
-                    channels.add(childChannel);
-                }
-            }
-        }
-
-        return channels;
-    }
-
-    /**
      * If this is a group, update the appearance of the children.
      */
     public void updateChildrenAppearance() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
index 1dd3739..6d656605 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
@@ -403,7 +403,6 @@
                 mChannelEditorDialogController,
                 packageName,
                 row.getEntry().getChannel(),
-                row.getUniqueChannels(),
                 row.getEntry(),
                 onSettingsClick,
                 onAppSettingsClick,
@@ -449,7 +448,6 @@
                 mChannelEditorDialogController,
                 packageName,
                 row.getEntry().getChannel(),
-                row.getUniqueChannels(),
                 row.getEntry(),
                 onSettingsClick,
                 mDeviceProvisionedController.isDeviceProvisioned(),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
index d8f31d4..d8ebd42 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
@@ -104,8 +104,6 @@
     private String mAppName;
     private int mAppUid;
     private String mDelegatePkg;
-    private int mNumUniqueChannelsInRow;
-    private Set<NotificationChannel> mUniqueChannelsInRow;
     private NotificationChannel mSingleNotificationChannel;
     private int mStartingChannelImportance;
     private boolean mWasShownHighPriority;
@@ -196,7 +194,6 @@
             ChannelEditorDialogController channelEditorDialogController,
             String pkg,
             NotificationChannel notificationChannel,
-            Set<NotificationChannel> uniqueChannelsInRow,
             NotificationEntry entry,
             OnSettingsClickListener onSettingsClick,
             OnAppSettingsClickListener onAppSettingsClick,
@@ -213,8 +210,6 @@
         mChannelEditorDialogController = channelEditorDialogController;
         mAssistantFeedbackController = assistantFeedbackController;
         mPackageName = pkg;
-        mUniqueChannelsInRow = uniqueChannelsInRow;
-        mNumUniqueChannelsInRow = uniqueChannelsInRow.size();
         mEntry = entry;
         mSbn = entry.getSbn();
         mPm = pm;
@@ -236,15 +231,8 @@
 
         int numTotalChannels = mINotificationManager.getNumNotificationChannelsForPackage(
                 pkg, mAppUid, false /* includeDeleted */);
-        if (mNumUniqueChannelsInRow == 0) {
-            throw new IllegalArgumentException("bindNotification requires at least one channel");
-        } else  {
-            // Special behavior for the Default channel if no other channels have been defined.
-            mIsSingleDefaultChannel = mNumUniqueChannelsInRow == 1
-                    && mSingleNotificationChannel.getId().equals(
-                            NotificationChannel.DEFAULT_CHANNEL_ID)
-                    && numTotalChannels == 1;
-        }
+        mIsSingleDefaultChannel = mSingleNotificationChannel.getId().equals(
+                NotificationChannel.DEFAULT_CHANNEL_ID) && numTotalChannels == 1;
         mIsAutomaticChosen = getAlertingBehavior() == BEHAVIOR_AUTOMATIC;
 
         bindHeader();
@@ -271,11 +259,6 @@
             findViewById(R.id.interruptiveness_settings).setVisibility(GONE);
             ((TextView) findViewById(R.id.done)).setText(R.string.inline_done_button);
             findViewById(R.id.turn_off_notifications).setVisibility(GONE);
-        } else if (mNumUniqueChannelsInRow > 1) {
-            findViewById(R.id.non_configurable_call_text).setVisibility(GONE);
-            findViewById(R.id.non_configurable_text).setVisibility(GONE);
-            findViewById(R.id.interruptiveness_settings).setVisibility(GONE);
-            findViewById(R.id.non_configurable_multichannel_text).setVisibility(VISIBLE);
         } else {
             findViewById(R.id.non_configurable_call_text).setVisibility(GONE);
             findViewById(R.id.non_configurable_text).setVisibility(GONE);
@@ -361,9 +344,7 @@
         if (mAppUid >= 0 && mOnSettingsClickListener != null && mIsDeviceProvisioned) {
             final int appUidF = mAppUid;
             return ((View view) -> {
-                mOnSettingsClickListener.onClick(view,
-                        mNumUniqueChannelsInRow > 1 ? null : mSingleNotificationChannel,
-                        appUidF);
+                mOnSettingsClickListener.onClick(view, mSingleNotificationChannel, appUidF);
             });
         }
         return null;
@@ -375,7 +356,7 @@
                 mPresentingChannelEditorDialog = true;
 
                 mChannelEditorDialogController.prepareDialogForApp(mAppName, mPackageName, mAppUid,
-                        mUniqueChannelsInRow, mPkgIcon, mOnSettingsClickListener);
+                        mSingleNotificationChannel, mPkgIcon, mOnSettingsClickListener);
                 mChannelEditorDialogController.setOnFinishListener(() -> {
                     mPresentingChannelEditorDialog = false;
                     mGutsContainer.closeControls(this, false);
@@ -392,7 +373,7 @@
 
     private void bindName() {
         final TextView channelName = findViewById(R.id.channel_name);
-        if (mIsSingleDefaultChannel || mNumUniqueChannelsInRow > 1) {
+        if (mIsSingleDefaultChannel) {
             channelName.setVisibility(View.GONE);
         } else {
             channelName.setText(mSingleNotificationChannel.getName());
@@ -459,7 +440,7 @@
             Handler bgHandler = new Handler(Dependency.get(Dependency.BG_LOOPER));
             bgHandler.post(
                     new UpdateImportanceRunnable(mINotificationManager, mPackageName, mAppUid,
-                            mNumUniqueChannelsInRow == 1 ? mSingleNotificationChannel : null,
+                            mSingleNotificationChannel,
                             mStartingChannelImportance, newImportance, mIsAutomaticChosen));
             mOnUserInteractionCallback.onImportanceChanged(mEntry);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PartialConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PartialConversationInfo.java
index 06c3b79..53f7d4b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PartialConversationInfo.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PartialConversationInfo.java
@@ -57,7 +57,6 @@
     private StatusBarNotification mSbn;
     private boolean mIsDeviceProvisioned;
     private boolean mIsNonBlockable;
-    private Set<NotificationChannel> mUniqueChannelsInRow;
     private Drawable mPkgIcon;
 
     private boolean mPresentingChannelEditorDialog = false;
@@ -83,7 +82,6 @@
             ChannelEditorDialogController channelEditorDialogController,
             String pkg,
             NotificationChannel notificationChannel,
-            Set<NotificationChannel> uniqueChannelsInRow,
             NotificationEntry entry,
             NotificationInfo.OnSettingsClickListener onSettingsClick,
             boolean isDeviceProvisioned,
@@ -100,7 +98,6 @@
         mIsDeviceProvisioned = isDeviceProvisioned;
         mIsNonBlockable = isNonBlockable;
         mChannelEditorDialogController = channelEditorDialogController;
-        mUniqueChannelsInRow = uniqueChannelsInRow;
 
         bindHeader();
         bindActions();
@@ -149,7 +146,7 @@
                 mPresentingChannelEditorDialog = true;
 
                 mChannelEditorDialogController.prepareDialogForApp(mAppName, mPackageName, mAppUid,
-                        mUniqueChannelsInRow, mPkgIcon, mOnSettingsClickListener);
+                        mNotificationChannel, mPkgIcon, mOnSettingsClickListener);
                 mChannelEditorDialogController.setOnFinishListener(() -> {
                     mPresentingChannelEditorDialog = false;
                     mGutsContainer.closeControls(this, false);
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/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
index cd68621..dc50990 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
@@ -71,6 +71,7 @@
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.flow.mapNotNull
@@ -181,6 +182,7 @@
                 telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
                 awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
             }
+            .flowOn(bgDispatcher)
             .scan(initial = initial) { state, event -> state.applyEvent(event) }
             .stateIn(scope = scope, started = SharingStarted.WhileSubscribed(), initial)
     }
@@ -358,6 +360,7 @@
 
                 awaitClose { context.unregisterReceiver(receiver) }
             }
+            .flowOn(bgDispatcher)
             .stateIn(scope, SharingStarted.WhileSubscribed(), defaultNetworkName)
 
     override val dataEnabled = run {
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 74a849a..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()
@@ -134,22 +137,24 @@
             )
             .stateIn(scope, started = SharingStarted.WhileSubscribed(), null)
 
-    private val mobileSubscriptionsChangeEvent: Flow<Unit> = conflatedCallbackFlow {
-        val callback =
-            object : SubscriptionManager.OnSubscriptionsChangedListener() {
-                override fun onSubscriptionsChanged() {
-                    logger.logOnSubscriptionsChanged()
-                    trySend(Unit)
-                }
+    private val mobileSubscriptionsChangeEvent: Flow<Unit> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : SubscriptionManager.OnSubscriptionsChangedListener() {
+                        override fun onSubscriptionsChanged() {
+                            logger.logOnSubscriptionsChanged()
+                            trySend(Unit)
+                        }
+                    }
+
+                subscriptionManager.addOnSubscriptionsChangedListener(
+                    bgDispatcher.asExecutor(),
+                    callback,
+                )
+
+                awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
             }
-
-        subscriptionManager.addOnSubscriptionsChangedListener(
-            bgDispatcher.asExecutor(),
-            callback,
-        )
-
-        awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
-    }
+            .flowOn(bgDispatcher)
 
     /**
      * State flow that emits the set of mobile data subscriptions, each represented by its own
@@ -184,6 +189,7 @@
                 telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
                 awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
             }
+            .flowOn(bgDispatcher)
             .distinctUntilChanged()
             .logDiffsForTable(
                 tableLogger,
@@ -250,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/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java
index 01fabcc..3008c866d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java
@@ -159,7 +159,8 @@
 
         // Update the rotation policy, if needed, for this new device state
         if (shouldBeLocked != isLocked) {
-            mRotationPolicyWrapper.setRotationLock(shouldBeLocked);
+            mRotationPolicyWrapper.setRotationLock(shouldBeLocked,
+                    /* caller= */"DeviceStateRotationLockSettingController#readPersistedSetting");
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java
index 518a9b3..e5f72eb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java
@@ -40,6 +40,7 @@
 import android.provider.DeviceConfig;
 import android.provider.Settings;
 
+import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
@@ -361,6 +362,7 @@
         private static final int MSG_ADD_CALLBACK = 3;
         private static final int MSG_REMOVE_CALLBACK = 4;
 
+        @GuardedBy("mSettingsChangeCallbacks")
         private final ArrayList<LocationChangeCallback> mSettingsChangeCallbacks =
                 new ArrayList<>();
 
@@ -378,10 +380,14 @@
                     locationActiveChanged();
                     break;
                 case MSG_ADD_CALLBACK:
-                    mSettingsChangeCallbacks.add((LocationChangeCallback) msg.obj);
+                    synchronized (mSettingsChangeCallbacks) {
+                        mSettingsChangeCallbacks.add((LocationChangeCallback) msg.obj);
+                    }
                     break;
                 case MSG_REMOVE_CALLBACK:
-                    mSettingsChangeCallbacks.remove((LocationChangeCallback) msg.obj);
+                    synchronized (mSettingsChangeCallbacks) {
+                        mSettingsChangeCallbacks.remove((LocationChangeCallback) msg.obj);
+                    }
                     break;
 
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockController.java
index 1158324..607f1e5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockController.java
@@ -24,8 +24,8 @@
     boolean isRotationLockAffordanceVisible();
     boolean isRotationLocked();
     boolean isCameraRotationEnabled();
-    void setRotationLocked(boolean locked);
-    void setRotationLockedAtAngle(boolean locked, int rotation);
+    void setRotationLocked(boolean locked, String caller);
+    void setRotationLockedAtAngle(boolean locked, int rotation, String caller);
 
     public interface RotationLockControllerCallback {
         void onRotationLockStateChanged(boolean rotationLocked, boolean affordanceVisible);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java
index 1eeb0ac..797aa1f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java
@@ -93,12 +93,12 @@
         return mRotationPolicy.isCameraRotationEnabled();
     }
 
-    public void setRotationLocked(boolean locked) {
-        mRotationPolicy.setRotationLock(locked);
+    public void setRotationLocked(boolean locked, String caller) {
+        mRotationPolicy.setRotationLock(locked, caller);
     }
 
-    public void setRotationLockedAtAngle(boolean locked, int rotation) {
-        mRotationPolicy.setRotationLockAtAngle(locked, rotation);
+    public void setRotationLockedAtAngle(boolean locked, int rotation, String caller) {
+        mRotationPolicy.setRotationLockAtAngle(locked, rotation, caller);
     }
 
     public boolean isRotationLockAffordanceVisible() {
diff --git a/packages/SystemUI/src/com/android/systemui/user/UserModule.java b/packages/SystemUI/src/com/android/systemui/user/UserModule.java
index d8ee686..348670f 100644
--- a/packages/SystemUI/src/com/android/systemui/user/UserModule.java
+++ b/packages/SystemUI/src/com/android/systemui/user/UserModule.java
@@ -21,7 +21,6 @@
 import com.android.settingslib.users.CreateUserDialogController;
 import com.android.settingslib.users.EditUserInfoController;
 import com.android.systemui.user.data.repository.UserRepositoryModule;
-import com.android.systemui.user.domain.interactor.HeadlessSystemUserModeModule;
 import com.android.systemui.user.ui.dialog.UserDialogModule;
 
 import dagger.Module;
@@ -34,7 +33,6 @@
         includes = {
                 UserDialogModule.class,
                 UserRepositoryModule.class,
-                HeadlessSystemUserModeModule.class,
         }
 )
 public abstract class UserModule {
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/UserDomainLayerModule.kt b/packages/SystemUI/src/com/android/systemui/user/domain/UserDomainLayerModule.kt
new file mode 100644
index 0000000..4122404
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/UserDomainLayerModule.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.user.domain
+
+import com.android.systemui.user.domain.interactor.HeadlessSystemUserModeModule
+import dagger.Module
+
+@Module(includes = [HeadlessSystemUserModeModule::class]) object UserDomainLayerModule
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
index e489499..ce9d6fd 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
@@ -37,7 +37,6 @@
 import com.android.internal.util.UserIcons
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
-import com.android.systemui.res.R
 import com.android.systemui.SystemUISecondaryUserService
 import com.android.systemui.animation.Expandable
 import com.android.systemui.broadcast.BroadcastDispatcher
@@ -50,6 +49,7 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.qs.user.UserSwitchDialogController
+import com.android.systemui.res.R
 import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
 import com.android.systemui.user.CreateUserActivity
 import com.android.systemui.user.data.model.UserSwitcherSettingsModel
@@ -62,6 +62,7 @@
 import com.android.systemui.user.utils.MultiUserActionsEvent
 import com.android.systemui.user.utils.MultiUserActionsEventHelper
 import com.android.systemui.util.kotlin.pairwise
+import com.android.systemui.utils.UserRestrictionChecker
 import java.io.PrintWriter
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
@@ -103,6 +104,7 @@
     private val refreshUsersScheduler: RefreshUsersScheduler,
     private val guestUserInteractor: GuestUserInteractor,
     private val uiEventLogger: UiEventLogger,
+    private val userRestrictionChecker: UserRestrictionChecker,
 ) {
     /**
      * Defines interface for classes that can be notified when the state of users on the device is
@@ -593,6 +595,7 @@
                 ) &&
                     // If the user is auto-created is must not be currently resetting.
                     !(isGuestUserAutoCreated && isGuestUserResetting),
+            userRestrictionChecker = userRestrictionChecker,
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt
index 93573fa..80139bd 100644
--- a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt
@@ -22,10 +22,10 @@
 import android.graphics.Bitmap
 import android.os.UserManager
 import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin
-import com.android.settingslib.RestrictedLockUtilsInternal
 import com.android.systemui.res.R
 import com.android.systemui.user.data.source.UserRecord
 import com.android.systemui.user.shared.model.UserActionModel
+import com.android.systemui.utils.UserRestrictionChecker
 
 /**
  * Defines utility functions for helping with legacy data code for users.
@@ -68,6 +68,7 @@
         actionType: UserActionModel,
         isRestricted: Boolean,
         isSwitchToEnabled: Boolean,
+        userRestrictionChecker: UserRestrictionChecker,
     ): UserRecord {
         return UserRecord(
             isGuest = actionType == UserActionModel.ENTER_GUEST_MODE,
@@ -79,6 +80,7 @@
                 getEnforcedAdmin(
                     context = context,
                     selectedUserId = selectedUserId,
+                    userRestrictionChecker = userRestrictionChecker,
                 ),
             isManageUsers = actionType == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
         )
@@ -103,9 +105,10 @@
     private fun getEnforcedAdmin(
         context: Context,
         selectedUserId: Int,
+        userRestrictionChecker: UserRestrictionChecker
     ): EnforcedAdmin? {
         val admin =
-            RestrictedLockUtilsInternal.checkIfRestrictionEnforced(
+            userRestrictionChecker.checkIfRestrictionEnforced(
                 context,
                 UserManager.DISALLOW_ADD_USER,
                 selectedUserId,
@@ -113,7 +116,7 @@
                 ?: return null
 
         return if (
-            !RestrictedLockUtilsInternal.hasBaseUserRestriction(
+            !userRestrictionChecker.hasBaseUserRestriction(
                 context,
                 UserManager.DISALLOW_ADD_USER,
                 selectedUserId,
diff --git a/packages/SystemUI/src/com/android/systemui/util/wrapper/RotationPolicyWrapper.kt b/packages/SystemUI/src/com/android/systemui/util/wrapper/RotationPolicyWrapper.kt
index d8de07d..374ebe0 100644
--- a/packages/SystemUI/src/com/android/systemui/util/wrapper/RotationPolicyWrapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/wrapper/RotationPolicyWrapper.kt
@@ -28,8 +28,8 @@
  * Testable wrapper interface around RotationPolicy {link com.android.internal.view.RotationPolicy}
  */
 interface RotationPolicyWrapper {
-    fun setRotationLock(enabled: Boolean)
-    fun setRotationLockAtAngle(enabled: Boolean, rotation: Int)
+    fun setRotationLock(enabled: Boolean, caller: String)
+    fun setRotationLockAtAngle(enabled: Boolean, rotation: Int, caller: String)
     fun getRotationLockOrientation(): Int
     fun isRotationLockToggleVisible(): Boolean
     fun isRotationLocked(): Boolean
@@ -44,14 +44,14 @@
 ) :
         RotationPolicyWrapper {
 
-    override fun setRotationLock(enabled: Boolean) {
+    override fun setRotationLock(enabled: Boolean, caller: String) {
         traceSection("RotationPolicyWrapperImpl#setRotationLock") {
-            RotationPolicy.setRotationLock(context, enabled)
+            RotationPolicy.setRotationLock(context, enabled, caller)
         }
     }
 
-    override fun setRotationLockAtAngle(enabled: Boolean, rotation: Int) {
-        RotationPolicy.setRotationLockAtAngle(context, enabled, rotation)
+    override fun setRotationLockAtAngle(enabled: Boolean, rotation: Int, caller: String) {
+        RotationPolicy.setRotationLockAtAngle(context, enabled, rotation, caller)
     }
 
     override fun getRotationLockOrientation(): Int =
diff --git a/packages/SystemUI/src/com/android/systemui/utils/UserRestrictionChecker.kt b/packages/SystemUI/src/com/android/systemui/utils/UserRestrictionChecker.kt
new file mode 100644
index 0000000..3f8346b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/utils/UserRestrictionChecker.kt
@@ -0,0 +1,25 @@
+package com.android.systemui.utils
+
+import android.content.Context
+import com.android.settingslib.RestrictedLockUtils
+import com.android.settingslib.RestrictedLockUtilsInternal
+import javax.inject.Inject
+
+/** Proxy to call [RestrictedLockUtilsInternal] */
+class UserRestrictionChecker @Inject constructor() {
+    fun checkIfRestrictionEnforced(
+        context: Context,
+        userRestriction: String,
+        userId: Int
+    ): RestrictedLockUtils.EnforcedAdmin? {
+        return RestrictedLockUtilsInternal.checkIfRestrictionEnforced(
+            context,
+            userRestriction,
+            userId
+        )
+    }
+
+    fun hasBaseUserRestriction(context: Context, userRestriction: String, userId: Int): Boolean {
+        return RestrictedLockUtilsInternal.hasBaseUserRestriction(context, userRestriction, userId)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/CoroutineTestScopeModule.kt b/packages/SystemUI/tests/src/com/android/CoroutineTestScopeModule.kt
new file mode 100644
index 0000000..360aa0f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/CoroutineTestScopeModule.kt
@@ -0,0 +1,56 @@
+/*
+ * 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
+
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import kotlin.coroutines.ContinuationInterceptor
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.TestScope
+
+@Module(includes = [CoroutineTestScopeModule.Bindings::class])
+class CoroutineTestScopeModule
+private constructor(
+    @get:Provides val scope: TestScope,
+    @get:Provides val dispatcher: TestDispatcher,
+    @get:Provides val scheduler: TestCoroutineScheduler = dispatcher.scheduler,
+) {
+
+    constructor() : this(TestScope())
+
+    constructor(
+        scope: TestScope
+    ) : this(scope, scope.coroutineContext[ContinuationInterceptor] as TestDispatcher)
+
+    constructor(context: CoroutineContext) : this(TestScope(context))
+
+    @get:[Provides Application]
+    val appScope: CoroutineScope = scope.backgroundScope
+
+    @Module
+    interface Bindings {
+        @Binds @Main fun bindMainDispatcher(dispatcher: TestDispatcher): CoroutineDispatcher
+        @Binds @Background fun bindBgDispatcher(dispatcher: TestDispatcher): CoroutineDispatcher
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/SysUITestModule.kt b/packages/SystemUI/tests/src/com/android/SysUITestModule.kt
new file mode 100644
index 0000000..ea74510
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/SysUITestModule.kt
@@ -0,0 +1,42 @@
+/*
+ * 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
+
+import android.content.Context
+import com.android.systemui.FakeSystemUiModule
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.dagger.qualifiers.Application
+import dagger.Module
+import dagger.Provides
+
+@Module(
+    includes =
+        [
+            TestMocksModule::class,
+            CoroutineTestScopeModule::class,
+            FakeSystemUiModule::class,
+        ]
+)
+class SysUITestModule {
+    @Provides fun provideContext(test: SysuiTestCase): Context = test.context
+
+    @Provides @Application fun provideAppContext(test: SysuiTestCase): Context = test.context
+
+    @Provides
+    fun provideBroadcastDispatcher(test: SysuiTestCase): BroadcastDispatcher =
+        test.fakeBroadcastDispatcher
+}
diff --git a/packages/SystemUI/tests/src/com/android/TestMocksModule.kt b/packages/SystemUI/tests/src/com/android/TestMocksModule.kt
new file mode 100644
index 0000000..8990583
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/TestMocksModule.kt
@@ -0,0 +1,78 @@
+/*
+ * 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
+
+import android.app.ActivityManager
+import android.app.admin.DevicePolicyManager
+import android.os.UserManager
+import com.android.keyguard.KeyguardSecurityModel
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.GuestResumeSessionReceiver
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.dagger.BroadcastDispatcherLog
+import com.android.systemui.log.dagger.SceneFrameworkLog
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.DarkIconDispatcher
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.NotificationListener
+import com.android.systemui.statusbar.NotificationMediaManager
+import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
+import com.android.systemui.statusbar.phone.DozeParameters
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.phone.ScreenOffAnimationController
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.statusbar.policy.SplitShadeStateController
+import com.android.systemui.statusbar.window.StatusBarWindowController
+import com.android.systemui.util.mockito.mock
+import com.android.wm.shell.bubbles.Bubbles
+import dagger.Module
+import dagger.Provides
+import java.util.Optional
+
+@Module
+data class TestMocksModule(
+    @get:Provides val activityStarter: ActivityStarter = mock(),
+    @get:Provides val bubbles: Optional<Bubbles> = Optional.of(mock()),
+    @get:Provides val configurationController: ConfigurationController = mock(),
+    @get:Provides val darkIconDispatcher: DarkIconDispatcher = mock(),
+    @get:Provides val demoModeController: DemoModeController = mock(),
+    @get:Provides val deviceProvisionedController: DeviceProvisionedController = mock(),
+    @get:Provides val dozeParameters: DozeParameters = mock(),
+    @get:Provides val guestResumeSessionReceiver: GuestResumeSessionReceiver = mock(),
+    @get:Provides val keyguardBypassController: KeyguardBypassController = mock(),
+    @get:Provides val keyguardSecurityModel: KeyguardSecurityModel = mock(),
+    @get:Provides val keyguardUpdateMonitor: KeyguardUpdateMonitor = mock(),
+    @get:Provides val notifListener: NotificationListener = mock(),
+    @get:Provides val notifMediaManager: NotificationMediaManager = mock(),
+    @get:Provides val screenOffAnimController: ScreenOffAnimationController = mock(),
+    @get:Provides val splitShadeStateController: SplitShadeStateController = mock(),
+    @get:Provides val statusBarStateController: StatusBarStateController = mock(),
+    @get:Provides val statusBarWindowController: StatusBarWindowController = mock(),
+    @get:Provides val wakeUpCoordinator: NotificationWakeUpCoordinator = mock(),
+
+    // log buffers
+    @get:[Provides BroadcastDispatcherLog]
+    val broadcastDispatcherLogger: LogBuffer = mock(),
+    @get:[Provides SceneFrameworkLog]
+    val sceneLogger: LogBuffer = mock(),
+
+    // framework mocks
+    @get:Provides val activityManager: ActivityManager = mock(),
+    @get:Provides val devicePolicyManager: DevicePolicyManager = mock(),
+    @get:Provides val userManager: UserManager = mock(),
+)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
index fc7d20a..874053a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
@@ -236,7 +236,8 @@
             utils.authenticationRepository.setAuthenticationMethod(
                 DataLayerAuthenticationMethodModel.Pin
             )
-            assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)).isTrue()
+            assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
+                .isEqualTo(AuthenticationResult.SUCCEEDED)
             assertThat(isThrottled).isFalse()
         }
 
@@ -246,7 +247,8 @@
             utils.authenticationRepository.setAuthenticationMethod(
                 DataLayerAuthenticationMethodModel.Pin
             )
-            assertThat(underTest.authenticate(listOf(9, 8, 7, 6, 5, 4))).isFalse()
+            assertThat(underTest.authenticate(listOf(9, 8, 7, 6, 5, 4)))
+                .isEqualTo(AuthenticationResult.FAILED)
         }
 
     @Test(expected = IllegalArgumentException::class)
@@ -267,7 +269,7 @@
                 overrideCredential(pin)
             }
 
-            assertThat(underTest.authenticate(pin)).isTrue()
+            assertThat(underTest.authenticate(pin)).isEqualTo(AuthenticationResult.SUCCEEDED)
         }
 
     @Test
@@ -282,7 +284,8 @@
             utils.authenticationRepository.setAuthenticationMethod(
                 DataLayerAuthenticationMethodModel.Pin
             )
-            assertThat(underTest.authenticate(List(17) { 9 })).isFalse()
+            assertThat(underTest.authenticate(List(17) { 9 }))
+                .isEqualTo(AuthenticationResult.FAILED)
         }
 
     @Test
@@ -293,7 +296,8 @@
                 DataLayerAuthenticationMethodModel.Password
             )
 
-            assertThat(underTest.authenticate("password".toList())).isTrue()
+            assertThat(underTest.authenticate("password".toList()))
+                .isEqualTo(AuthenticationResult.SUCCEEDED)
             assertThat(isThrottled).isFalse()
         }
 
@@ -304,7 +308,8 @@
                 DataLayerAuthenticationMethodModel.Password
             )
 
-            assertThat(underTest.authenticate("alohomora".toList())).isFalse()
+            assertThat(underTest.authenticate("alohomora".toList()))
+                .isEqualTo(AuthenticationResult.FAILED)
         }
 
     @Test
@@ -314,7 +319,8 @@
                 DataLayerAuthenticationMethodModel.Pattern
             )
 
-            assertThat(underTest.authenticate(FakeAuthenticationRepository.PATTERN)).isTrue()
+            assertThat(underTest.authenticate(FakeAuthenticationRepository.PATTERN))
+                .isEqualTo(AuthenticationResult.SUCCEEDED)
         }
 
     @Test
@@ -327,22 +333,14 @@
             assertThat(
                     underTest.authenticate(
                         listOf(
-                            AuthenticationPatternCoordinate(
-                                x = 2,
-                                y = 0,
-                            ),
-                            AuthenticationPatternCoordinate(
-                                x = 2,
-                                y = 1,
-                            ),
-                            AuthenticationPatternCoordinate(
-                                x = 2,
-                                y = 2,
-                            ),
+                            AuthenticationPatternCoordinate(x = 2, y = 0),
+                            AuthenticationPatternCoordinate(x = 2, y = 1),
+                            AuthenticationPatternCoordinate(x = 2, y = 2),
+                            AuthenticationPatternCoordinate(x = 1, y = 2),
                         )
                     )
                 )
-                .isFalse()
+                .isEqualTo(AuthenticationResult.FAILED)
         }
 
     @Test
@@ -361,7 +359,7 @@
                         tryAutoConfirm = true
                     )
                 )
-                .isNull()
+                .isEqualTo(AuthenticationResult.SKIPPED)
             assertThat(isThrottled).isFalse()
         }
 
@@ -379,7 +377,7 @@
                         tryAutoConfirm = true
                     )
                 )
-                .isFalse()
+                .isEqualTo(AuthenticationResult.FAILED)
             assertThat(isUnlocked).isFalse()
         }
 
@@ -397,7 +395,7 @@
                         tryAutoConfirm = true
                     )
                 )
-                .isFalse()
+                .isEqualTo(AuthenticationResult.FAILED)
             assertThat(isUnlocked).isFalse()
         }
 
@@ -415,7 +413,7 @@
                         tryAutoConfirm = true
                     )
                 )
-                .isTrue()
+                .isEqualTo(AuthenticationResult.SUCCEEDED)
             assertThat(isUnlocked).isTrue()
         }
 
@@ -433,7 +431,7 @@
                         tryAutoConfirm = true
                     )
                 )
-                .isNull()
+                .isEqualTo(AuthenticationResult.SKIPPED)
             assertThat(isUnlocked).isFalse()
         }
 
@@ -445,7 +443,8 @@
                 DataLayerAuthenticationMethodModel.Password
             )
 
-            assertThat(underTest.authenticate("password".toList(), tryAutoConfirm = true)).isNull()
+            assertThat(underTest.authenticate("password".toList(), tryAutoConfirm = true))
+                .isEqualTo(AuthenticationResult.SKIPPED)
             assertThat(isUnlocked).isFalse()
         }
 
@@ -490,7 +489,8 @@
                 )
 
             // Correct PIN, but throttled, so doesn't attempt it:
-            assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)).isNull()
+            assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
+                .isEqualTo(AuthenticationResult.SKIPPED)
             assertThat(isUnlocked).isFalse()
             assertThat(isThrottled).isTrue()
             assertThat(throttling)
@@ -536,7 +536,8 @@
                 )
 
             // Correct PIN and no longer throttled so unlocks successfully:
-            assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)).isTrue()
+            assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
+                .isEqualTo(AuthenticationResult.SUCCEEDED)
             assertThat(isUnlocked).isTrue()
             assertThat(isThrottled).isFalse()
             assertThat(throttling).isEqualTo(AuthenticationThrottlingModel())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt
index 8fc63b2..7365460 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt
@@ -44,6 +44,7 @@
 import com.android.systemui.user.domain.interactor.HeadlessSystemUserMode
 import com.android.systemui.user.domain.interactor.RefreshUsersScheduler
 import com.android.systemui.user.domain.interactor.UserInteractor
+import com.android.systemui.util.mockito.mock
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
@@ -126,6 +127,7 @@
                 refreshUsersScheduler = refreshUsersScheduler,
                 guestUserInteractor = guestInteractor,
                 uiEventLogger = uiEventLogger,
+                userRestrictionChecker = mock(),
             )
         shadeInteractor =
             ShadeInteractor(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
index 77d8102..92c8a39 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
@@ -17,13 +17,14 @@
 package com.android.systemui.bouncer.domain.interactor
 
 import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.model.AuthenticationMethodModel
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
+import com.android.systemui.authentication.domain.interactor.AuthenticationResult
 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
 import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.res.R
 import com.android.systemui.scene.SceneTestUtils
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
@@ -87,7 +88,8 @@
             assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
 
             // Wrong input.
-            assertThat(underTest.authenticate(listOf(9, 8, 7))).isFalse()
+            assertThat(underTest.authenticate(listOf(9, 8, 7)))
+                .isEqualTo(AuthenticationResult.FAILED)
             assertThat(message).isEqualTo(MESSAGE_WRONG_PIN)
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
 
@@ -95,7 +97,8 @@
             assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
 
             // Correct input.
-            assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)).isTrue()
+            assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
+                .isEqualTo(AuthenticationResult.SUCCEEDED)
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
         }
 
@@ -115,13 +118,14 @@
             underTest.clearMessage()
 
             // Incomplete input.
-            assertThat(underTest.authenticate(listOf(1, 2), tryAutoConfirm = true)).isNull()
+            assertThat(underTest.authenticate(listOf(1, 2), tryAutoConfirm = true))
+                .isEqualTo(AuthenticationResult.SKIPPED)
             assertThat(message).isEmpty()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
 
             // Wrong 6-digit pin
             assertThat(underTest.authenticate(listOf(1, 2, 3, 5, 5, 6), tryAutoConfirm = true))
-                .isFalse()
+                .isEqualTo(AuthenticationResult.FAILED)
             assertThat(message).isEqualTo(MESSAGE_WRONG_PIN)
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
 
@@ -132,7 +136,7 @@
                         tryAutoConfirm = true
                     )
                 )
-                .isTrue()
+                .isEqualTo(AuthenticationResult.SUCCEEDED)
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
         }
 
@@ -150,7 +154,8 @@
             underTest.clearMessage()
 
             // Incomplete input.
-            assertThat(underTest.authenticate(listOf(1, 2), tryAutoConfirm = true)).isNull()
+            assertThat(underTest.authenticate(listOf(1, 2), tryAutoConfirm = true))
+                .isEqualTo(AuthenticationResult.SKIPPED)
             assertThat(message).isEmpty()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
 
@@ -161,7 +166,7 @@
                         tryAutoConfirm = true
                     )
                 )
-                .isNull()
+                .isEqualTo(AuthenticationResult.SKIPPED)
             assertThat(message).isEmpty()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
         }
@@ -187,7 +192,8 @@
             assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
 
             // Wrong input.
-            assertThat(underTest.authenticate("alohamora".toList())).isFalse()
+            assertThat(underTest.authenticate("alohamora".toList()))
+                .isEqualTo(AuthenticationResult.FAILED)
             assertThat(message).isEqualTo(MESSAGE_WRONG_PASSWORD)
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
 
@@ -195,7 +201,8 @@
             assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
 
             // Correct input.
-            assertThat(underTest.authenticate("password".toList())).isTrue()
+            assertThat(underTest.authenticate("password".toList()))
+                .isEqualTo(AuthenticationResult.SUCCEEDED)
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
         }
 
@@ -220,8 +227,30 @@
             assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
 
             // Wrong input.
-            assertThat(underTest.authenticate(listOf(AuthenticationPatternCoordinate(1, 2))))
-                .isFalse()
+            val wrongPattern =
+                listOf(
+                    AuthenticationPatternCoordinate(1, 2),
+                    AuthenticationPatternCoordinate(1, 1),
+                    AuthenticationPatternCoordinate(0, 0),
+                    AuthenticationPatternCoordinate(0, 1),
+                )
+            assertThat(wrongPattern).isNotEqualTo(FakeAuthenticationRepository.PATTERN)
+            assertThat(wrongPattern.size).isAtLeast(utils.authenticationRepository.minPatternLength)
+            assertThat(underTest.authenticate(wrongPattern)).isEqualTo(AuthenticationResult.FAILED)
+            assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN)
+            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+
+            underTest.resetMessage()
+            assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
+
+            // Too short input.
+            val tooShortPattern =
+                FakeAuthenticationRepository.PATTERN.subList(
+                    0,
+                    utils.authenticationRepository.minPatternLength - 1
+                )
+            assertThat(underTest.authenticate(tooShortPattern))
+                .isEqualTo(AuthenticationResult.SKIPPED)
             assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN)
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
 
@@ -229,7 +258,8 @@
             assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
 
             // Correct input.
-            assertThat(underTest.authenticate(FakeAuthenticationRepository.PATTERN)).isTrue()
+            assertThat(underTest.authenticate(FakeAuthenticationRepository.PATTERN))
+                .isEqualTo(AuthenticationResult.SUCCEEDED)
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
         }
 
@@ -294,7 +324,8 @@
             assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
             repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING) { times ->
                 // Wrong PIN.
-                assertThat(underTest.authenticate(listOf(6, 7, 8, 9))).isFalse()
+                assertThat(underTest.authenticate(listOf(6, 7, 8, 9)))
+                    .isEqualTo(AuthenticationResult.FAILED)
                 if (
                     times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING - 1
                 ) {
@@ -317,7 +348,8 @@
             )
 
             // Correct PIN, but throttled, so doesn't change away from the bouncer scene:
-            assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)).isNull()
+            assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
+                .isEqualTo(AuthenticationResult.SKIPPED)
             assertThat(currentScene?.key).isEqualTo(SceneKey.Bouncer)
             assertTryAgainMessage(
                 message,
@@ -347,7 +379,8 @@
             assertThat(currentScene?.key).isEqualTo(SceneKey.Bouncer)
 
             // Correct PIN and no longer throttled so changes to the Gone scene:
-            assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)).isTrue()
+            assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
+                .isEqualTo(AuthenticationResult.SUCCEEDED)
             assertThat(currentScene?.key).isEqualTo(SceneKey.Gone)
             assertThat(isThrottled).isFalse()
             assertThat(throttling).isEqualTo(AuthenticationThrottlingModel())
@@ -362,7 +395,7 @@
             val bouncerSceneKey = currentScene?.key
             assertThat(bouncerSceneKey).isEqualTo(SceneKey.Bouncer)
 
-            underTest.hide("")
+            underTest.onImeHidden()
 
             assertThat(currentScene?.key).isEqualTo(SceneKey.Lockscreen)
         }
@@ -376,7 +409,7 @@
             val notBouncerSceneKey = currentScene?.key
             assertThat(notBouncerSceneKey).isNotEqualTo(SceneKey.Bouncer)
 
-            underTest.hide("")
+            underTest.onImeHidden()
 
             assertThat(currentScene?.key).isEqualTo(notBouncerSceneKey)
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
index 9011c2f..2f7dde0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
@@ -45,7 +45,7 @@
     private val underTest =
         PinBouncerViewModel(
             applicationContext = context,
-            applicationScope = testScope.backgroundScope,
+            viewModelScope = testScope.backgroundScope,
             interactor =
                 utils.bouncerInteractor(
                     authenticationInteractor = authenticationInteractor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
index 2c96bcc..da2534d6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
@@ -46,7 +46,7 @@
     private val testScope = utils.testScope
     private val authenticationInteractor =
         utils.authenticationInteractor(
-            repository = utils.authenticationRepository(),
+            repository = utils.authenticationRepository,
         )
     private val bouncerInteractor =
         utils.bouncerInteractor(
@@ -66,7 +66,8 @@
 
             authMethodsToTest().forEach { authMethod ->
                 utils.authenticationRepository.setAuthenticationMethod(authMethod)
-                val job = underTest.authMethod.onEach { authMethodViewModel = it }.launchIn(this)
+                val job =
+                    underTest.authMethodViewModel.onEach { authMethodViewModel = it }.launchIn(this)
                 runCurrent()
 
                 if (authMethod.isSecure) {
@@ -86,22 +87,43 @@
         }
 
     @Test
-    fun authMethod_reusesInstances() =
+    fun authMethodChanged_doesNotReuseInstances() =
         testScope.runTest {
             val seen =
                 mutableMapOf<DomainLayerAuthenticationMethodModel, AuthMethodBouncerViewModel>()
             val authMethodViewModel: AuthMethodBouncerViewModel? by
-                collectLastValue(underTest.authMethod)
+                collectLastValue(underTest.authMethodViewModel)
+
             // First pass, populate our "seen" map:
             authMethodsToTest().forEach { authMethod ->
                 utils.authenticationRepository.setAuthenticationMethod(authMethod)
                 authMethodViewModel?.let { seen[authMethod] = it }
             }
 
-            // Second pass, assert same instances are reused:
+            // Second pass, assert same instances are not reused:
             authMethodsToTest().forEach { authMethod ->
                 utils.authenticationRepository.setAuthenticationMethod(authMethod)
-                authMethodViewModel?.let { assertThat(it).isSameInstanceAs(seen[authMethod]) }
+                authMethodViewModel?.let {
+                    assertThat(it.authenticationMethod).isEqualTo(authMethod)
+                    assertThat(it).isNotSameInstanceAs(seen[authMethod])
+                }
+            }
+        }
+
+    @Test
+    fun authMethodUnchanged_reusesInstances() =
+        testScope.runTest {
+            authMethodsToTest().forEach { authMethod ->
+                utils.authenticationRepository.setAuthenticationMethod(authMethod)
+                val firstInstance: AuthMethodBouncerViewModel? =
+                    collectLastValue(underTest.authMethodViewModel).invoke()
+
+                utils.authenticationRepository.setAuthenticationMethod(authMethod)
+                val secondInstance: AuthMethodBouncerViewModel? =
+                    collectLastValue(underTest.authMethodViewModel).invoke()
+
+                firstInstance?.let { assertThat(it.authenticationMethod).isEqualTo(authMethod) }
+                assertThat(secondInstance).isSameInstanceAs(firstInstance)
             }
         }
 
@@ -136,7 +158,7 @@
         testScope.runTest {
             val isInputEnabled by
                 collectLastValue(
-                    underTest.authMethod.flatMapLatest { authViewModel ->
+                    underTest.authMethodViewModel.flatMapLatest { authViewModel ->
                         authViewModel?.isInputEnabled ?: emptyFlow()
                     }
                 )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
index 3375184..c1b3354 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
@@ -17,10 +17,11 @@
 package com.android.systemui.bouncer.ui.viewmodel
 
 import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.model.AuthenticationMethodModel
+import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainAuthenticationMethodModel
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.res.R
 import com.android.systemui.scene.SceneTestUtils
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
@@ -28,6 +29,7 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -59,7 +61,7 @@
         )
     private val underTest =
         PasswordBouncerViewModel(
-            applicationScope = testScope.backgroundScope,
+            viewModelScope = testScope.backgroundScope,
             interactor = bouncerInteractor,
             isInputEnabled = MutableStateFlow(true).asStateFlow(),
         )
@@ -76,19 +78,13 @@
             val currentScene by collectLastValue(sceneInteractor.desiredScene)
             val message by collectLastValue(bouncerViewModel.message)
             val password by collectLastValue(underTest.password)
-            utils.authenticationRepository.setAuthenticationMethod(
-                AuthenticationMethodModel.Password
-            )
-            utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
-            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-
-            underTest.onShown()
+            lockDeviceAndOpenPasswordBouncer()
 
             assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD)
             assertThat(password).isEqualTo("")
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+            assertThat(underTest.authenticationMethod)
+                .isEqualTo(DomainAuthenticationMethodModel.Password)
         }
 
     @Test
@@ -97,15 +93,7 @@
             val currentScene by collectLastValue(sceneInteractor.desiredScene)
             val message by collectLastValue(bouncerViewModel.message)
             val password by collectLastValue(underTest.password)
-            utils.authenticationRepository.setAuthenticationMethod(
-                AuthenticationMethodModel.Password
-            )
-            utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
-            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-            underTest.onShown()
-            runCurrent()
+            lockDeviceAndOpenPasswordBouncer()
 
             underTest.onPasswordInputChanged("password")
 
@@ -118,16 +106,9 @@
     fun onAuthenticateKeyPressed_whenCorrect() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.desiredScene)
-            utils.authenticationRepository.setAuthenticationMethod(
-                AuthenticationMethodModel.Password
-            )
-            utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
-            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-            underTest.onShown()
-            underTest.onPasswordInputChanged("password")
+            lockDeviceAndOpenPasswordBouncer()
 
+            underTest.onPasswordInputChanged("password")
             underTest.onAuthenticateKeyPressed()
 
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
@@ -139,16 +120,9 @@
             val currentScene by collectLastValue(sceneInteractor.desiredScene)
             val message by collectLastValue(bouncerViewModel.message)
             val password by collectLastValue(underTest.password)
-            utils.authenticationRepository.setAuthenticationMethod(
-                AuthenticationMethodModel.Password
-            )
-            utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
-            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-            underTest.onShown()
-            underTest.onPasswordInputChanged("wrong")
+            lockDeviceAndOpenPasswordBouncer()
 
+            underTest.onPasswordInputChanged("wrong")
             underTest.onAuthenticateKeyPressed()
 
             assertThat(password).isEqualTo("")
@@ -185,14 +159,9 @@
             val currentScene by collectLastValue(sceneInteractor.desiredScene)
             val message by collectLastValue(bouncerViewModel.message)
             val password by collectLastValue(underTest.password)
-            utils.authenticationRepository.setAuthenticationMethod(
-                AuthenticationMethodModel.Password
-            )
-            utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
-            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-            underTest.onShown()
+            lockDeviceAndOpenPasswordBouncer()
+
+            // Enter the wrong password:
             underTest.onPasswordInputChanged("wrong")
             underTest.onAuthenticateKeyPressed()
             assertThat(password).isEqualTo("")
@@ -213,14 +182,7 @@
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.desiredScene)
             val password by collectLastValue(underTest.password)
-            utils.authenticationRepository.setAuthenticationMethod(
-                AuthenticationMethodModel.Password
-            )
-            utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
-            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-            underTest.onShown()
+            lockDeviceAndOpenPasswordBouncer()
 
             // The user types a password.
             underTest.onPasswordInputChanged("password")
@@ -243,6 +205,18 @@
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
         }
 
+    private fun TestScope.lockDeviceAndOpenPasswordBouncer() {
+        utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Password)
+        utils.authenticationRepository.setUnlocked(false)
+        sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
+        sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
+
+        assertThat(collectLastValue(sceneInteractor.desiredScene).invoke())
+            .isEqualTo(SceneModel(SceneKey.Bouncer))
+        underTest.onShown()
+        runCurrent()
+    }
+
     companion object {
         private const val ENTER_YOUR_PASSWORD = "Enter your password"
         private const val WRONG_PASSWORD = "Wrong password"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index 102cfe2..bf109d9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -17,12 +17,13 @@
 package com.android.systemui.bouncer.ui.viewmodel
 
 import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.model.AuthenticationMethodModel
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
+import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainAuthenticationMethodModel
 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate as Point
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.res.R
 import com.android.systemui.scene.SceneTestUtils
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
@@ -64,7 +65,7 @@
     private val underTest =
         PatternBouncerViewModel(
             applicationContext = context,
-            applicationScope = testScope.backgroundScope,
+            viewModelScope = testScope.backgroundScope,
             interactor = bouncerInteractor,
             isInputEnabled = MutableStateFlow(true).asStateFlow(),
         )
@@ -85,14 +86,14 @@
             val message by collectLastValue(bouncerViewModel.message)
             val selectedDots by collectLastValue(underTest.selectedDots)
             val currentDot by collectLastValue(underTest.currentDot)
-            transitionToPatternBouncer()
-
-            underTest.onShown()
+            lockDeviceAndOpenPatternBouncer()
 
             assertThat(message?.text).isEqualTo(ENTER_YOUR_PATTERN)
             assertThat(selectedDots).isEmpty()
             assertThat(currentDot).isNull()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+            assertThat(underTest.authenticationMethod)
+                .isEqualTo(DomainAuthenticationMethodModel.Pattern)
         }
 
     @Test
@@ -102,9 +103,7 @@
             val message by collectLastValue(bouncerViewModel.message)
             val selectedDots by collectLastValue(underTest.selectedDots)
             val currentDot by collectLastValue(underTest.currentDot)
-            transitionToPatternBouncer()
-            underTest.onShown()
-            runCurrent()
+            lockDeviceAndOpenPatternBouncer()
 
             underTest.onDragStart()
 
@@ -120,8 +119,7 @@
             val currentScene by collectLastValue(sceneInteractor.desiredScene)
             val selectedDots by collectLastValue(underTest.selectedDots)
             val currentDot by collectLastValue(underTest.currentDot)
-            transitionToPatternBouncer()
-            underTest.onShown()
+            lockDeviceAndOpenPatternBouncer()
             underTest.onDragStart()
             assertThat(currentDot).isNull()
             CORRECT_PATTERN.forEachIndexed { index, coordinate ->
@@ -158,8 +156,7 @@
             val message by collectLastValue(bouncerViewModel.message)
             val selectedDots by collectLastValue(underTest.selectedDots)
             val currentDot by collectLastValue(underTest.currentDot)
-            transitionToPatternBouncer()
-            underTest.onShown()
+            lockDeviceAndOpenPatternBouncer()
             underTest.onDragStart()
             CORRECT_PATTERN.subList(0, 3).forEach { coordinate -> dragToCoordinate(coordinate) }
 
@@ -175,8 +172,7 @@
     fun onDrag_shouldIncludeDotsThatWereSkippedOverAlongTheSameRow() =
         testScope.runTest {
             val selectedDots by collectLastValue(underTest.selectedDots)
-            transitionToPatternBouncer()
-            underTest.onShown()
+            lockDeviceAndOpenPatternBouncer()
 
             /*
              * Pattern setup, coordinates are (column, row)
@@ -202,8 +198,7 @@
     fun onDrag_shouldIncludeDotsThatWereSkippedOverAlongTheSameColumn() =
         testScope.runTest {
             val selectedDots by collectLastValue(underTest.selectedDots)
-            transitionToPatternBouncer()
-            underTest.onShown()
+            lockDeviceAndOpenPatternBouncer()
 
             /*
              * Pattern setup, coordinates are (column, row)
@@ -229,8 +224,7 @@
     fun onDrag_shouldIncludeDotsThatWereSkippedOverAlongTheDiagonal() =
         testScope.runTest {
             val selectedDots by collectLastValue(underTest.selectedDots)
-            transitionToPatternBouncer()
-            underTest.onShown()
+            lockDeviceAndOpenPatternBouncer()
 
             /*
              * Pattern setup
@@ -258,8 +252,7 @@
     fun onDrag_shouldNotIncludeDotIfItIsNotOnTheLine() =
         testScope.runTest {
             val selectedDots by collectLastValue(underTest.selectedDots)
-            transitionToPatternBouncer()
-            underTest.onShown()
+            lockDeviceAndOpenPatternBouncer()
 
             /*
              * Pattern setup
@@ -287,8 +280,7 @@
     fun onDrag_shouldNotIncludeSkippedOverDotsIfTheyAreAlreadySelected() =
         testScope.runTest {
             val selectedDots by collectLastValue(underTest.selectedDots)
-            transitionToPatternBouncer()
-            underTest.onShown()
+            lockDeviceAndOpenPatternBouncer()
 
             /*
              * Pattern setup
@@ -315,20 +307,10 @@
     @Test
     fun onDragEnd_whenPatternTooShort() =
         testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
             val message by collectLastValue(bouncerViewModel.message)
-            val selectedDots by collectLastValue(underTest.selectedDots)
-            val currentDot by collectLastValue(underTest.currentDot)
             val throttlingDialogMessage by
                 collectLastValue(bouncerViewModel.throttlingDialogMessage)
-            utils.authenticationRepository.setAuthenticationMethod(
-                AuthenticationMethodModel.Pattern
-            )
-            utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
-            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-            underTest.onShown()
+            lockDeviceAndOpenPatternBouncer()
 
             // Enter a pattern that's too short more than enough times that would normally trigger
             // throttling if the pattern were not too short and wrong:
@@ -337,7 +319,7 @@
                 underTest.onDragStart()
                 CORRECT_PATTERN.subList(
                         0,
-                        authenticationInteractor.minPatternLength - 1,
+                        utils.authenticationRepository.minPatternLength - 1,
                     )
                     .forEach { coordinate ->
                         underTest.onDrag(
@@ -362,10 +344,10 @@
             val message by collectLastValue(bouncerViewModel.message)
             val selectedDots by collectLastValue(underTest.selectedDots)
             val currentDot by collectLastValue(underTest.currentDot)
-            transitionToPatternBouncer()
-            underTest.onShown()
+            lockDeviceAndOpenPatternBouncer()
+
             underTest.onDragStart()
-            CORRECT_PATTERN.subList(2, 7).forEach { coordinate -> dragToCoordinate(coordinate) }
+            CORRECT_PATTERN.subList(2, 7).forEach(::dragToCoordinate)
             underTest.onDragEnd()
             assertThat(selectedDots).isEmpty()
             assertThat(currentDot).isNull()
@@ -373,7 +355,7 @@
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
 
             // Enter the correct pattern:
-            CORRECT_PATTERN.forEach { coordinate -> dragToCoordinate(coordinate) }
+            CORRECT_PATTERN.forEach(::dragToCoordinate)
 
             underTest.onDragEnd()
 
@@ -382,7 +364,7 @@
 
     private fun dragOverCoordinates(vararg coordinatesDragged: Point) {
         underTest.onDragStart()
-        coordinatesDragged.forEach { dragToCoordinate(it) }
+        coordinatesDragged.forEach(::dragToCoordinate)
     }
 
     private fun dragToCoordinate(coordinate: Point) {
@@ -394,13 +376,15 @@
         )
     }
 
-    private fun TestScope.transitionToPatternBouncer() {
+    private fun TestScope.lockDeviceAndOpenPatternBouncer() {
         utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pattern)
         utils.authenticationRepository.setUnlocked(false)
         sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
         sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
         assertThat(collectLastValue(sceneInteractor.desiredScene).invoke())
             .isEqualTo(SceneModel(SceneKey.Bouncer))
+        underTest.onShown()
+        runCurrent()
     }
 
     companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index 35238ce..2576204 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -17,11 +17,12 @@
 package com.android.systemui.bouncer.ui.viewmodel
 
 import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.model.AuthenticationMethodModel
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
+import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainAuthenticationMethodModel
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.res.R
 import com.android.systemui.scene.SceneTestUtils
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
@@ -30,6 +31,7 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -62,7 +64,7 @@
     private val underTest =
         PinBouncerViewModel(
             applicationContext = context,
-            applicationScope = testScope.backgroundScope,
+            viewModelScope = testScope.backgroundScope,
             interactor = bouncerInteractor,
             isInputEnabled = MutableStateFlow(true).asStateFlow(),
         )
@@ -90,6 +92,8 @@
             assertThat(message?.text).isEqualTo(ENTER_YOUR_PIN)
             assertThat(pin).isEmpty()
             assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+            assertThat(underTest.authenticationMethod)
+                .isEqualTo(DomainAuthenticationMethodModel.Pin)
         }
 
     @Test
@@ -120,14 +124,8 @@
             val currentScene by collectLastValue(sceneInteractor.desiredScene)
             val message by collectLastValue(bouncerViewModel.message)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
-            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
-            utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
-            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
+            lockDeviceAndOpenPinBouncer()
 
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-            underTest.onShown()
-            runCurrent()
             underTest.onPinButtonClicked(1)
             assertThat(pin).hasSize(1)
 
@@ -141,15 +139,8 @@
     @Test
     fun onPinEdit() =
         testScope.runTest {
-            val currentScene by collectLastValue(sceneInteractor.desiredScene)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
-            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
-            utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
-            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
-
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-            underTest.onShown()
+            lockDeviceAndOpenPinBouncer()
 
             underTest.onPinButtonClicked(1)
             underTest.onPinButtonClicked(2)
@@ -168,18 +159,13 @@
             val currentScene by collectLastValue(sceneInteractor.desiredScene)
             val message by collectLastValue(bouncerViewModel.message)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
-            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
-            utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
-            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
+            lockDeviceAndOpenPinBouncer()
 
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-            underTest.onShown()
-            runCurrent()
             underTest.onPinButtonClicked(1)
             underTest.onPinButtonClicked(2)
             underTest.onPinButtonClicked(3)
             underTest.onPinButtonClicked(4)
+            runCurrent()
 
             underTest.onBackspaceButtonLongPressed()
 
@@ -192,13 +178,8 @@
     fun onAuthenticateButtonClicked_whenCorrect() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.desiredScene)
-            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
-            utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
-            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
+            lockDeviceAndOpenPinBouncer()
 
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-            underTest.onShown()
             FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit ->
                 underTest.onPinButtonClicked(digit)
             }
@@ -214,13 +195,8 @@
             val currentScene by collectLastValue(sceneInteractor.desiredScene)
             val message by collectLastValue(bouncerViewModel.message)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
-            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
-            utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
-            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
+            lockDeviceAndOpenPinBouncer()
 
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-            underTest.onShown()
             underTest.onPinButtonClicked(1)
             underTest.onPinButtonClicked(2)
             underTest.onPinButtonClicked(3)
@@ -240,13 +216,8 @@
             val currentScene by collectLastValue(sceneInteractor.desiredScene)
             val message by collectLastValue(bouncerViewModel.message)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
-            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
-            utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
-            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
+            lockDeviceAndOpenPinBouncer()
 
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-            underTest.onShown()
             underTest.onPinButtonClicked(1)
             underTest.onPinButtonClicked(2)
             underTest.onPinButtonClicked(3)
@@ -272,14 +243,9 @@
     fun onAutoConfirm_whenCorrect() =
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.desiredScene)
-            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
-            utils.authenticationRepository.setUnlocked(false)
             utils.authenticationRepository.setAutoConfirmEnabled(true)
-            sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
-            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
+            lockDeviceAndOpenPinBouncer()
 
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-            underTest.onShown()
             FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit ->
                 underTest.onPinButtonClicked(digit)
             }
@@ -293,14 +259,9 @@
             val currentScene by collectLastValue(sceneInteractor.desiredScene)
             val message by collectLastValue(bouncerViewModel.message)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
-            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
-            utils.authenticationRepository.setUnlocked(false)
             utils.authenticationRepository.setAutoConfirmEnabled(true)
-            sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
-            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
+            lockDeviceAndOpenPinBouncer()
 
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-            underTest.onShown()
             FakeAuthenticationRepository.DEFAULT_PIN.dropLast(1).forEach { digit ->
                 underTest.onPinButtonClicked(digit)
             }
@@ -318,13 +279,7 @@
         testScope.runTest {
             val currentScene by collectLastValue(sceneInteractor.desiredScene)
             val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
-            utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
-            utils.authenticationRepository.setUnlocked(false)
-            sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
-            sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
-
-            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-            underTest.onShown()
+            lockDeviceAndOpenPinBouncer()
 
             // The user types a PIN.
             FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit ->
@@ -401,6 +356,18 @@
             assertThat(confirmButtonAppearance).isEqualTo(ActionButtonAppearance.Hidden)
         }
 
+    private fun TestScope.lockDeviceAndOpenPinBouncer() {
+        utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+        utils.authenticationRepository.setUnlocked(false)
+        sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
+        sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
+
+        assertThat(collectLastValue(sceneInteractor.desiredScene).invoke())
+            .isEqualTo(SceneModel(SceneKey.Bouncer))
+        underTest.onShown()
+        runCurrent()
+    }
+
     companion object {
         private const val ENTER_YOUR_PIN = "Enter your pin"
         private const val WRONG_PIN = "Wrong pin"
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/media/controls/ui/MediaCarouselScrollHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandlerTest.kt
index 49f536e..74b3fce 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandlerTest.kt
@@ -85,6 +85,7 @@
     fun testCarouselScroll_shortScroll() {
         whenever(mediaCarousel.isLayoutRtl).thenReturn(false)
         whenever(mediaCarousel.relativeScrollX).thenReturn(300)
+        whenever(mediaCarousel.scrollX).thenReturn(300)
 
         mediaCarousel.touchListener?.onTouchEvent(motionEventUp)
         executor.runAllReady()
@@ -96,6 +97,7 @@
     fun testCarouselScroll_shortScroll_isRTL() {
         whenever(mediaCarousel.isLayoutRtl).thenReturn(true)
         whenever(mediaCarousel.relativeScrollX).thenReturn(300)
+        whenever(mediaCarousel.scrollX).thenReturn(carouselWidth - 300)
 
         mediaCarousel.touchListener?.onTouchEvent(motionEventUp)
         executor.runAllReady()
@@ -107,6 +109,7 @@
     fun testCarouselScroll_longScroll() {
         whenever(mediaCarousel.isLayoutRtl).thenReturn(false)
         whenever(mediaCarousel.relativeScrollX).thenReturn(600)
+        whenever(mediaCarousel.scrollX).thenReturn(600)
 
         mediaCarousel.touchListener?.onTouchEvent(motionEventUp)
         executor.runAllReady()
@@ -118,6 +121,7 @@
     fun testCarouselScroll_longScroll_isRTL() {
         whenever(mediaCarousel.isLayoutRtl).thenReturn(true)
         whenever(mediaCarousel.relativeScrollX).thenReturn(600)
+        whenever(mediaCarousel.scrollX).thenReturn(carouselWidth - 600)
 
         mediaCarousel.touchListener?.onTouchEvent(motionEventUp)
         executor.runAllReady()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt
index 435a1f1..5eda263 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt
@@ -57,10 +57,7 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
-        repository =
-            FakePowerRepository(
-                initialInteractive = true,
-            )
+        repository = FakePowerRepository()
         underTest =
             PowerInteractor(
                 repository,
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/qs/tiles/DeviceControlsTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DeviceControlsTileTest.kt
index bce4c06..3bf59ca 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DeviceControlsTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DeviceControlsTileTest.kt
@@ -172,6 +172,9 @@
     @Test
     fun testNotAvailableControls() {
         featureEnabled = false
+
+        // Destroy previous tile
+        tile.destroy()
         tile = createTile()
 
         assertThat(tile.isAvailable).isFalse()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DreamTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DreamTileTest.java
index a0c1073..954d30ed 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DreamTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DreamTileTest.java
@@ -226,6 +226,10 @@
         assertTrue(supportedTileOnlySystemUser.isAvailable());
         when(mUserTracker.getUserInfo()).thenReturn(nonMainUserInfo);
         assertFalse(supportedTileOnlySystemUser.isAvailable());
+
+        destroyTile(unsupportedTile);
+        destroyTile(supportedTileAllUsers);
+        destroyTile(supportedTileOnlySystemUser);
     }
 
     @Test
@@ -250,6 +254,8 @@
         mTestableLooper.processAllMessages();
         assertEquals(QSTileImpl.ResourceIcon.get(R.drawable.ic_qs_screen_saver_undocked),
                 dockedTile.getState().icon);
+
+        destroyTile(dockedTile);
     }
 
     private void setScreensaverEnabled(boolean enabled) {
@@ -257,6 +263,11 @@
                 DEFAULT_USER);
     }
 
+    private void destroyTile(QSTileImpl<?> tile) {
+        tile.destroy();
+        mTestableLooper.processAllMessages();
+    }
+
     private DreamTile constructTileForTest(boolean dreamSupported,
             boolean dreamOnlyEnabledForSystemUser) {
         return new DreamTile(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java
index df6993d..440270b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java
@@ -216,7 +216,7 @@
     public void testSecondaryString_rotationResolverDisabled_isEmpty() {
         mTestableResources.addOverride(com.android.internal.R.bool.config_allowRotationResolver,
                 false);
-        mLockTile = new RotationLockTile(
+        RotationLockTile otherTile = new RotationLockTile(
                 mHost,
                 mUiEventLogger,
                 mTestableLooper.getLooper(),
@@ -232,10 +232,12 @@
                 new FakeSettings()
         );
 
-        mLockTile.refreshState();
+        otherTile.refreshState();
         mTestableLooper.processAllMessages();
 
-        assertEquals("", mLockTile.getState().secondaryLabel.toString());
+        assertEquals("", otherTile.getState().secondaryLabel.toString());
+
+        destroyTile(otherTile);
     }
 
     @Test
@@ -258,6 +260,12 @@
         assertEquals(state.icon, QSTileImpl.ResourceIcon.get(R.drawable.qs_auto_rotate_icon_on));
     }
 
+
+    private void destroyTile(QSTileImpl<?> tile) {
+        tile.destroy();
+        mTestableLooper.processAllMessages();
+    }
+
     private void enableAutoRotation() {
         when(mRotationPolicyWrapper.isRotationLocked()).thenReturn(false);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index 6b918c6..85bd92b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -76,6 +76,7 @@
  *   being used when the state is as required (e.g. cannot unlock an already unlocked device, cannot
  *   put to sleep a device that's already asleep, etc.).
  */
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(JUnit4::class)
 class SceneFrameworkIntegrationTest : SysuiTestCase() {
@@ -481,7 +482,7 @@
         bouncerSceneJob =
             if (to.key == SceneKey.Bouncer) {
                 testScope.backgroundScope.launch {
-                    bouncerViewModel.authMethod.collect {
+                    bouncerViewModel.authMethodViewModel.collect {
                         // Do nothing. Need this to turn this otherwise cold flow, hot.
                     }
                 }
@@ -556,7 +557,7 @@
         assertWithMessage("Cannot enter PIN when not on the Bouncer scene!")
             .that(getCurrentSceneInUi())
             .isEqualTo(SceneKey.Bouncer)
-        val authMethodViewModel by collectLastValue(bouncerViewModel.authMethod)
+        val authMethodViewModel by collectLastValue(bouncerViewModel.authMethodViewModel)
         assertWithMessage("Cannot enter PIN when not using a PIN authentication method!")
             .that(authMethodViewModel)
             .isInstanceOf(PinBouncerViewModel::class.java)
@@ -613,11 +614,12 @@
     private fun TestScope.dismissIme(
         showImeBeforeDismissing: Boolean = true,
     ) {
-        if (showImeBeforeDismissing) {
-            bouncerViewModel.authMethod.value?.onImeVisibilityChanged(true)
+        bouncerViewModel.authMethodViewModel.value?.apply {
+            if (showImeBeforeDismissing) {
+                onImeVisibilityChanged(true)
+            }
+            onImeVisibilityChanged(false)
+            runCurrent()
         }
-
-        bouncerViewModel.authMethod.value?.onImeVisibilityChanged(false)
-        runCurrent()
     }
 }
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/shade/data/repository/ShadeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt
index d018cbb..2be1c09 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt
@@ -25,7 +25,6 @@
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.UiEventLogger
 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.coroutines.collectLastValue
@@ -35,6 +34,7 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.res.R
 import com.android.systemui.scene.SceneTestUtils
 import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags
 import com.android.systemui.scene.shared.model.ObservableTransitionState
@@ -54,6 +54,7 @@
 import com.android.systemui.user.domain.interactor.HeadlessSystemUserMode
 import com.android.systemui.user.domain.interactor.RefreshUsersScheduler
 import com.android.systemui.user.domain.interactor.UserInteractor
+import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -153,6 +154,7 @@
                 refreshUsersScheduler = refreshUsersScheduler,
                 guestUserInteractor = guestInteractor,
                 uiEventLogger = uiEventLogger,
+                userRestrictionChecker = mock(),
             )
         underTest =
             ShadeInteractor(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
index ac2aec6..0a10b2c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.withArgCaptor
 import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
 import org.junit.Before
 import org.junit.Test
 import org.mockito.Mockito.never
@@ -126,6 +127,22 @@
     }
 
     @Test
+    fun testExpandUnattachedEntry() {
+        featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true)
+
+        // First, expand the entry when it is attached.
+        gem.setGroupExpanded(summary1, true)
+        assertThat(gem.isGroupExpanded(summary1)).isTrue()
+
+        // Un-attach it, and un-expand it.
+        NotificationEntryBuilder.setNewParent(summary1, null)
+        gem.setGroupExpanded(summary1, false)
+
+        // Expanding again should throw.
+        assertThrows(IllegalArgumentException::class.java) { gem.setGroupExpanded(summary1, true) }
+    }
+
+    @Test
     fun testSyncWithPipeline() {
         featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true)
         gem.attach(pipeline)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt
index 37ec0e0..c1ffa64 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt
@@ -58,6 +58,35 @@
         val noParentEntry = NotificationEntryBuilder().setParent(null).build()
         assertThat(gmm.isChildInGroup(noParentEntry)).isFalse()
     }
+    @Test
+    fun testIsChildInGroup_summary_old() {
+        featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, false)
+
+        val groupKey = "group"
+        val summary =
+            NotificationEntryBuilder()
+                .setGroup(mContext, groupKey)
+                .setGroupSummary(mContext, true)
+                .build()
+        GroupEntryBuilder().setKey(groupKey).setSummary(summary).build()
+
+        assertThat(gmm.isChildInGroup(summary)).isTrue()
+    }
+
+    @Test
+    fun testIsChildInGroup_summary_new() {
+        featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true)
+
+        val groupKey = "group"
+        val summary =
+            NotificationEntryBuilder()
+                .setGroup(mContext, groupKey)
+                .setGroupSummary(mContext, true)
+                .build()
+        GroupEntryBuilder().setKey(groupKey).setSummary(summary).build()
+
+        assertThat(gmm.isChildInGroup(summary)).isFalse()
+    }
 
     @Test
     fun testIsChildInGroup_child() {
@@ -67,40 +96,78 @@
     }
 
     @Test
-    fun testIsGroupSummary() {
+    fun testIsGroupSummary_topLevelEntry() {
         featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true)
-        val entry = NotificationEntryBuilder().setGroupSummary(mContext, true).build()
-        assertThat(gmm.isGroupSummary(entry)).isTrue()
+        val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build()
+        assertThat(gmm.isGroupSummary(entry)).isFalse()
     }
 
     @Test
-    fun testGetGroupSummary() {
+    fun testIsGroupSummary_summary() {
         featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true)
 
+        val groupKey = "group"
         val summary =
             NotificationEntryBuilder()
-                .setGroup(mContext, "group")
+                .setGroup(mContext, groupKey)
                 .setGroupSummary(mContext, true)
                 .build()
-        val groupEntry =
-            GroupEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).setSummary(summary).build()
-        val entry =
-            NotificationEntryBuilder().setGroup(mContext, "group").setParent(groupEntry).build()
+        GroupEntryBuilder().setKey(groupKey).setSummary(summary).build()
 
-        assertThat(gmm.getGroupSummary(entry)).isEqualTo(summary)
+        assertThat(gmm.isGroupSummary(summary)).isTrue()
     }
 
     @Test
-    fun testGetGroupSummary_isSummary_old() {
-        featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, false)
-        val entry = NotificationEntryBuilder().setGroupSummary(mContext, true).build()
+    fun testIsGroupSummary_child() {
+        featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true)
+
+        val groupKey = "group"
+        val summary =
+            NotificationEntryBuilder()
+                .setGroup(mContext, groupKey)
+                .setGroupSummary(mContext, true)
+                .build()
+        val entry = NotificationEntryBuilder().setGroup(mContext, groupKey).build()
+        GroupEntryBuilder().setKey(groupKey).setSummary(summary).addChild(entry).build()
+
+        assertThat(gmm.isGroupSummary(entry)).isFalse()
+    }
+
+    @Test
+    fun testGetGroupSummary_topLevelEntry() {
+        featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true)
+        val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build()
         assertThat(gmm.getGroupSummary(entry)).isNull()
     }
 
     @Test
-    fun testGetGroupSummary_isSummary_new() {
+    fun testGetGroupSummary_summary() {
         featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true)
-        val entry = NotificationEntryBuilder().setGroupSummary(mContext, true).build()
-        assertThat(gmm.getGroupSummary(entry)).isEqualTo(entry)
+
+        val groupKey = "group"
+        val summary =
+            NotificationEntryBuilder()
+                .setGroup(mContext, groupKey)
+                .setGroupSummary(mContext, true)
+                .build()
+        GroupEntryBuilder().setKey(groupKey).setSummary(summary).build()
+
+        assertThat(gmm.getGroupSummary(summary)).isEqualTo(summary)
+    }
+
+    @Test
+    fun testGetGroupSummary_child() {
+        featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true)
+
+        val groupKey = "group"
+        val summary =
+            NotificationEntryBuilder()
+                .setGroup(mContext, groupKey)
+                .setGroupSummary(mContext, true)
+                .build()
+        val entry = NotificationEntryBuilder().setGroup(mContext, groupKey).build()
+        GroupEntryBuilder().setKey(groupKey).setSummary(summary).addChild(entry).build()
+
+        assertThat(gmm.getGroupSummary(entry)).isEqualTo(summary)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImplTest.kt
index b8792a8..126e0e8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImplTest.kt
@@ -18,26 +18,19 @@
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
 import androidx.test.filters.SmallTest
+import com.android.SysUITestModule
+import com.android.TestMocksModule
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.demomode.DemoModeController
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.plugins.DarkIconDispatcher
-import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.statusbar.NotificationListener
-import com.android.systemui.statusbar.NotificationMediaManager
-import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
-import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerShelfViewModel
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel
+import com.android.systemui.biometrics.domain.BiometricsDomainLayerModule
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FakeFeatureFlagsClassicModule
+import com.android.systemui.flags.Flags
 import com.android.systemui.statusbar.phone.DozeParameters
-import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.statusbar.phone.NotificationIconContainer
-import com.android.systemui.statusbar.phone.ScreenOffAnimationController
-import com.android.systemui.statusbar.window.StatusBarWindowController
+import com.android.systemui.user.domain.UserDomainLayerModule
 import com.android.systemui.util.mockito.whenever
-import com.android.wm.shell.bubbles.Bubbles
-import java.util.Optional
+import dagger.BindsInstance
+import dagger.Component
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Before
@@ -51,50 +44,32 @@
 @RunWith(AndroidTestingRunner::class)
 @RunWithLooper(setAsMainLooper = true)
 class NotificationIconAreaControllerViewBinderWrapperImplTest : SysuiTestCase() {
-    @Mock private lateinit var notifListener: NotificationListener
-    @Mock private lateinit var statusBarStateController: StatusBarStateController
-    @Mock private lateinit var wakeUpCoordinator: NotificationWakeUpCoordinator
-    @Mock private lateinit var keyguardBypassController: KeyguardBypassController
-    @Mock private lateinit var notifMediaManager: NotificationMediaManager
+
     @Mock private lateinit var dozeParams: DozeParameters
-    @Mock private lateinit var sectionStyleProvider: SectionStyleProvider
-    @Mock private lateinit var darkIconDispatcher: DarkIconDispatcher
-    @Mock private lateinit var statusBarWindowController: StatusBarWindowController
-    @Mock private lateinit var screenOffAnimController: ScreenOffAnimationController
-    @Mock private lateinit var bubbles: Bubbles
-    @Mock private lateinit var demoModeController: DemoModeController
     @Mock private lateinit var aodIcons: NotificationIconContainer
-    @Mock private lateinit var featureFlags: FeatureFlags
 
-    private val shelfViewModel = NotificationIconContainerShelfViewModel()
-    private val statusBarViewModel = NotificationIconContainerStatusBarViewModel()
-    private val aodViewModel = NotificationIconContainerAlwaysOnDisplayViewModel()
-
-    private lateinit var underTest: NotificationIconAreaControllerViewBinderWrapperImpl
+    private lateinit var testComponent: TestComponent
+    private val underTest
+        get() = testComponent.underTest
 
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
-        underTest =
-            NotificationIconAreaControllerViewBinderWrapperImpl(
-                mContext,
-                statusBarStateController,
-                wakeUpCoordinator,
-                keyguardBypassController,
-                notifMediaManager,
-                notifListener,
-                dozeParams,
-                sectionStyleProvider,
-                Optional.of(bubbles),
-                demoModeController,
-                darkIconDispatcher,
-                featureFlags,
-                statusBarWindowController,
-                screenOffAnimController,
-                shelfViewModel,
-                statusBarViewModel,
-                aodViewModel,
-            )
+        allowTestableLooperAsMainThread()
+
+        testComponent =
+            DaggerNotificationIconAreaControllerViewBinderWrapperImplTest_TestComponent.factory()
+                .create(
+                    test = this,
+                    featureFlags =
+                        FakeFeatureFlagsClassicModule {
+                            set(Flags.FACE_AUTH_REFACTOR, value = false)
+                        },
+                    mocks =
+                        TestMocksModule(
+                            dozeParameters = dozeParams,
+                        ),
+                )
     }
 
     @Test
@@ -117,4 +92,27 @@
         verify(aodIcons).translationY = 0f
         verify(aodIcons).alpha = 1.0f
     }
+
+    @SysUISingleton
+    @Component(
+        modules =
+            [
+                SysUITestModule::class,
+                BiometricsDomainLayerModule::class,
+                UserDomainLayerModule::class,
+            ]
+    )
+    interface TestComponent {
+
+        val underTest: NotificationIconAreaControllerViewBinderWrapperImpl
+
+        @Component.Factory
+        interface Factory {
+            fun create(
+                @BindsInstance test: SysuiTestCase,
+                mocks: TestMocksModule,
+                featureFlags: FakeFeatureFlagsClassicModule,
+            ): TestComponent
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
index f05436f..50ce265 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
@@ -71,6 +71,7 @@
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider.FullScreenIntentDecision;
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent;
 import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
@@ -117,6 +118,8 @@
     PendingIntent mPendingIntent;
     @Mock
     UserTracker mUserTracker;
+    @Mock
+    DeviceProvisionedController mDeviceProvisionedController;
 
     private NotificationInterruptStateProviderImpl mNotifInterruptionStateProvider;
 
@@ -141,7 +144,8 @@
                         mFlags,
                         mKeyguardNotificationVisibilityProvider,
                         mUiEventLoggerFake,
-                        mUserTracker);
+                        mUserTracker,
+                        mDeviceProvisionedController);
         mNotifInterruptionStateProvider.mUseHeadsUp = true;
     }
 
@@ -694,6 +698,25 @@
     }
 
     @Test
+    public void testShouldFullscreen_suppressedInterruptionsWhenNotProvisioned() {
+        NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
+        when(mPowerManager.isInteractive()).thenReturn(true);
+        when(mStatusBarStateController.getState()).thenReturn(SHADE);
+        when(mStatusBarStateController.isDreaming()).thenReturn(false);
+        when(mPowerManager.isScreenOn()).thenReturn(true);
+        when(mDeviceProvisionedController.isDeviceProvisioned()).thenReturn(false);
+        mNotifInterruptionStateProvider.addSuppressor(mSuppressInterruptions);
+
+        assertThat(mNotifInterruptionStateProvider.getFullScreenIntentDecision(entry))
+                .isEqualTo(FullScreenIntentDecision.FSI_NOT_PROVISIONED);
+        assertThat(mNotifInterruptionStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
+                .isTrue();
+        verify(mLogger, never()).logNoFullscreen(any(), any());
+        verify(mLogger, never()).logNoFullscreenWarning(any(), any());
+        verify(mLogger).logFullscreen(entry, "FSI_NOT_PROVISIONED");
+    }
+
+    @Test
     public void testShouldNotFullScreen_willHun() throws RemoteException {
         NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
         when(mPowerManager.isInteractive()).thenReturn(true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt
index 64d0256..7dcbd80 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt
@@ -73,14 +73,14 @@
         controller = ChannelEditorDialogController(mContext, mockNoMan, dialogBuilder)
 
         channel1 = NotificationChannel(TEST_CHANNEL, TEST_CHANNEL_NAME, IMPORTANCE_DEFAULT)
-        channel2 = NotificationChannel(TEST_CHANNEL2, TEST_CHANNEL_NAME2, IMPORTANCE_DEFAULT)
+        channel2 = NotificationChannel(TEST_CHANNEL2, TEST_CHANNEL_NAME2, IMPORTANCE_NONE)
         channelDefault = NotificationChannel(
                 NotificationChannel.DEFAULT_CHANNEL_ID, TEST_CHANNEL_NAME, IMPORTANCE_DEFAULT)
 
         group = NotificationChannelGroup(TEST_GROUP_ID, TEST_GROUP_NAME)
 
-        `when`(mockNoMan.getNotificationChannelGroupsForPackage(
-                eq(TEST_PACKAGE_NAME), eq(TEST_UID), anyBoolean()))
+        `when`(mockNoMan.getRecentBlockedNotificationChannelGroupsForPackage(
+                eq(TEST_PACKAGE_NAME), eq(TEST_UID)))
                 .thenReturn(ParceledListSlice(listOf(group)))
 
         `when`(mockNoMan.areNotificationsEnabledForPackage(eq(TEST_PACKAGE_NAME), eq(TEST_UID)))
@@ -89,11 +89,13 @@
 
     @Test
     fun testPrepareDialogForApp_noExtraChannels() {
+        channel1.group = group.id
+        channel2.group = group.id
         group.channels = listOf(channel1, channel2)
         controller.prepareDialogForApp(TEST_APP_NAME, TEST_PACKAGE_NAME, TEST_UID,
-                setOf(channel1, channel2), appIcon, clickListener)
+                channel1, appIcon, clickListener)
 
-        assertEquals(2, controller.paddedChannels.size)
+        assertEquals(2, controller.channelList.size)
     }
 
     @Test
@@ -101,39 +103,41 @@
         group.addChannel(channelDefault)
 
         controller.prepareDialogForApp(TEST_APP_NAME, TEST_PACKAGE_NAME, TEST_UID,
-                setOf(channelDefault), appIcon, clickListener)
+                channelDefault, appIcon, clickListener)
 
         assertEquals("No channels should be shown when there is only the miscellaneous channel",
-                0, controller.paddedChannels.size)
+                0, controller.channelList.size)
     }
 
     @Test
-    fun testPrepareDialogForApp_noProvidedChannels_noException() {
-        group.channels = listOf()
-
-        controller.prepareDialogForApp(TEST_APP_NAME, TEST_PACKAGE_NAME, TEST_UID,
-                setOf(), appIcon, clickListener)
-    }
-
-    @Test
-    fun testPrepareDialogForApp_retrievesUpTo4Channels() {
+    fun testPrepareDialogForApp_AddsAllChannelsAllGroups() {
+        val group2 = NotificationChannelGroup("two", "group two")
         val channel3 = NotificationChannel("test_channel_3", "Test channel 3", IMPORTANCE_DEFAULT)
+        channel3.group = group2.id
         val channel4 = NotificationChannel("test_channel_4", "Test channel 4", IMPORTANCE_DEFAULT)
+        channel4.group = group.id
+        val channel5 = NotificationChannel("test_channel_5", "Test channel 5", IMPORTANCE_DEFAULT)
+        channel5.group = group.id
 
-        group.channels = listOf(channel1, channel2, channel3, channel4)
+        `when`(mockNoMan.getRecentBlockedNotificationChannelGroupsForPackage(
+                eq(TEST_PACKAGE_NAME), eq(TEST_UID)))
+                .thenReturn(ParceledListSlice(listOf(group, group2)))
+
+        group.channels = listOf(channel1, channel2, channel4, channel5)
+        group2.channels = listOf(channel3)
 
         controller.prepareDialogForApp(TEST_APP_NAME, TEST_PACKAGE_NAME, TEST_UID,
-                setOf(channel1), appIcon, clickListener)
+                channel1, appIcon, clickListener)
 
-        assertEquals("ChannelEditorDialog should fetch enough channels to show 4",
-                4, controller.paddedChannels.size)
+        assertEquals("ChannelEditorDialog should show all channels",
+                5, controller.channelList.size)
     }
 
     @Test
     fun testApply_demoteChannel() {
         group.channels = listOf(channel1, channel2)
         controller.prepareDialogForApp(TEST_APP_NAME, TEST_PACKAGE_NAME, TEST_UID,
-                setOf(channel1, channel2), appIcon, clickListener)
+                channel1, appIcon, clickListener)
 
         // propose an adjustment of channel1
         controller.proposeEditForChannel(channel1, IMPORTANCE_NONE)
@@ -145,14 +149,33 @@
 
         // Channel 2 shouldn't have changed
         assertEquals("Proposed edits should take effect after apply",
+                IMPORTANCE_NONE, channel2.importance)
+    }
+
+    @Test
+    fun testApply_promoteChannel() {
+        group.channels = listOf(channel1, channel2)
+        controller.prepareDialogForApp(TEST_APP_NAME, TEST_PACKAGE_NAME, TEST_UID,
+                channel1, appIcon, clickListener)
+
+        // propose an adjustment of channel1
+        controller.proposeEditForChannel(channel2, IMPORTANCE_DEFAULT)
+
+        controller.apply()
+
+        assertEquals("Proposed edits should take effect after apply",
                 IMPORTANCE_DEFAULT, channel2.importance)
+
+        // Channel 1 shouldn't have changed
+        assertEquals("Proposed edits should take effect after apply",
+                IMPORTANCE_DEFAULT, channel1.importance)
     }
 
     @Test
     fun testApply_demoteApp() {
         group.channels = listOf(channel1, channel2)
         controller.prepareDialogForApp(TEST_APP_NAME, TEST_PACKAGE_NAME, TEST_UID,
-                setOf(channel1, channel2), appIcon, clickListener)
+                channel1, appIcon, clickListener)
 
         controller.proposeSetAppNotificationsEnabled(false)
         controller.apply()
@@ -168,7 +191,7 @@
                 .thenReturn(false)
 
         controller.prepareDialogForApp(TEST_APP_NAME, TEST_PACKAGE_NAME, TEST_UID,
-                setOf(channel1, channel2), appIcon, clickListener)
+                channel1, appIcon, clickListener)
         controller.proposeSetAppNotificationsEnabled(true)
         controller.apply()
 
@@ -181,7 +204,7 @@
         // GIVEN editor dialog
         group.channels = listOf(channel1, channel2)
         controller.prepareDialogForApp(TEST_APP_NAME, TEST_PACKAGE_NAME, TEST_UID,
-                setOf(channel1, channel2), appIcon, null)
+                channel1, appIcon, null)
 
         // WHEN user taps settings
         // Pass in any old view, it should never actually be used
@@ -197,7 +220,7 @@
 
         group.channels = listOf(channel1, channel2)
         controller.prepareDialogForApp(TEST_APP_NAME, TEST_PACKAGE_NAME, TEST_UID,
-                setOf(channel1, channel2), appIcon, null)
+                channel1, appIcon, null)
 
         // WHEN the user proposes a change
         controller.proposeEditForChannel(channel1, IMPORTANCE_NONE)
@@ -214,7 +237,7 @@
 
         group.channels = listOf(channel1, channel2)
         controller.prepareDialogForApp(TEST_APP_NAME, TEST_PACKAGE_NAME, TEST_UID,
-                setOf(channel1, channel2), appIcon, null)
+                channel1, appIcon, null)
 
         // WHEN the user proposes a change
         controller.proposeEditForChannel(channel1, IMPORTANCE_NONE)
@@ -236,7 +259,6 @@
         const val TEST_PACKAGE_NAME = "test_package"
         const val TEST_SYSTEM_PACKAGE_NAME = PRINT_SPOOLER_PACKAGE_NAME
         const val TEST_UID = 1
-        const val MULTIPLE_CHANNEL_COUNT = 2
         const val TEST_CHANNEL = "test_channel"
         const val TEST_CHANNEL_NAME = "Test Channel Name"
         const val TEST_CHANNEL2 = "test_channel2"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
index ac8b42a..e373143 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
@@ -481,33 +481,6 @@
     }
 
     @Test
-    public void testGetNumUniqueChildren_defaultChannel() throws Exception {
-        ExpandableNotificationRow groupRow = mNotificationTestHelper.createGroup();
-
-        assertEquals(1, groupRow.getNumUniqueChannels());
-    }
-
-    @Test
-    public void testGetNumUniqueChildren_multiChannel() throws Exception {
-        ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
-
-        List<ExpandableNotificationRow> childRows =
-                group.getChildrenContainer().getAttachedChildren();
-        // Give each child a unique channel id/name.
-        int i = 0;
-        for (ExpandableNotificationRow childRow : childRows) {
-            modifyRanking(childRow.getEntry())
-                    .setChannel(
-                            new NotificationChannel(
-                                    "id" + i, "dinnertime" + i, IMPORTANCE_DEFAULT))
-                    .build();
-            i++;
-        }
-
-        assertEquals(3, group.getNumUniqueChannels());
-    }
-
-    @Test
     public void testIconScrollXAfterTranslationAndReset() throws Exception {
         ExpandableNotificationRow group = mNotificationTestHelper.createGroup();
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
index 9e0f83c..0f1e63f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
@@ -462,7 +462,6 @@
                 eq(mChannelEditorDialogController),
                 eq(statusBarNotification.getPackageName()),
                 any(NotificationChannel.class),
-                anySet(),
                 eq(entry),
                 any(NotificationInfo.OnSettingsClickListener.class),
                 any(NotificationInfo.OnAppSettingsClickListener.class),
@@ -496,7 +495,6 @@
                 eq(mChannelEditorDialogController),
                 eq(statusBarNotification.getPackageName()),
                 any(NotificationChannel.class),
-                anySet(),
                 eq(entry),
                 any(NotificationInfo.OnSettingsClickListener.class),
                 any(NotificationInfo.OnAppSettingsClickListener.class),
@@ -528,7 +526,6 @@
                 eq(mChannelEditorDialogController),
                 eq(statusBarNotification.getPackageName()),
                 any(NotificationChannel.class),
-                anySet(),
                 eq(entry),
                 any(NotificationInfo.OnSettingsClickListener.class),
                 any(NotificationInfo.OnAppSettingsClickListener.class),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java
index f0b4dd4..b59385c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java
@@ -92,7 +92,6 @@
     private static final String TEST_PACKAGE_NAME = "test_package";
     private static final String TEST_SYSTEM_PACKAGE_NAME = PRINT_SPOOLER_PACKAGE_NAME;
     private static final int TEST_UID = 1;
-    private static final int MULTIPLE_CHANNEL_COUNT = 2;
     private static final String TEST_CHANNEL = "test_channel";
     private static final String TEST_CHANNEL_NAME = "TEST CHANNEL NAME";
 
@@ -100,8 +99,6 @@
     private NotificationInfo mNotificationInfo;
     private NotificationChannel mNotificationChannel;
     private NotificationChannel mDefaultNotificationChannel;
-    private Set<NotificationChannel> mNotificationChannelSet = new HashSet<>();
-    private Set<NotificationChannel> mDefaultNotificationChannelSet = new HashSet<>();
     private StatusBarNotification mSbn;
     private NotificationEntry mEntry;
     private UiEventLoggerFake mUiEventLogger = new UiEventLoggerFake();
@@ -162,11 +159,9 @@
         // Some test channels.
         mNotificationChannel = new NotificationChannel(
                 TEST_CHANNEL, TEST_CHANNEL_NAME, IMPORTANCE_LOW);
-        mNotificationChannelSet.add(mNotificationChannel);
         mDefaultNotificationChannel = new NotificationChannel(
                 NotificationChannel.DEFAULT_CHANNEL_ID, TEST_CHANNEL_NAME,
                 IMPORTANCE_LOW);
-        mDefaultNotificationChannelSet.add(mDefaultNotificationChannel);
         mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, 0,
                 new Notification(), UserHandle.getUserHandleForUid(TEST_UID), null, 0);
         mEntry = new NotificationEntryBuilder().setSbn(mSbn).build();
@@ -185,7 +180,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -212,7 +206,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -235,7 +228,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -267,7 +259,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 entry,
                 null,
                 null,
@@ -291,7 +282,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -320,7 +310,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -344,7 +333,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -367,7 +355,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mDefaultNotificationChannel,
-                mDefaultNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -394,7 +381,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mDefaultNotificationChannel,
-                mDefaultNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -417,7 +403,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -441,7 +426,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 (View v, NotificationChannel c, int appUid) -> {
                     assertEquals(mNotificationChannel, c);
@@ -470,7 +454,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -494,7 +477,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 (View v, NotificationChannel c, int appUid) -> {
                     assertEquals(mNotificationChannel, c);
@@ -519,7 +501,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -536,7 +517,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 (View v, NotificationChannel c, int appUid) -> { },
                 null,
@@ -551,86 +531,6 @@
     }
 
     @Test
-    public void testOnClickListenerPassesNullChannelForBundle() throws Exception {
-        final CountDownLatch latch = new CountDownLatch(1);
-        mNotificationInfo.bindNotification(
-                mMockPackageManager,
-                mMockINotificationManager,
-                mOnUserInteractionCallback,
-                mChannelEditorDialogController,
-                TEST_PACKAGE_NAME, mNotificationChannel,
-                createMultipleChannelSet(MULTIPLE_CHANNEL_COUNT),
-                mEntry,
-                (View v, NotificationChannel c, int appUid) -> {
-                    assertEquals(null, c);
-                    latch.countDown();
-                },
-                null,
-                mUiEventLogger,
-                true,
-                true,
-                true,
-                mAssistantFeedbackController,
-                mMetricsLogger);
-
-        mNotificationInfo.findViewById(R.id.info).performClick();
-        // Verify that listener was triggered.
-        assertEquals(0, latch.getCount());
-    }
-
-    @Test
-    @UiThreadTest
-    public void testBindNotification_ChannelNameInvisibleWhenBundleFromDifferentChannels()
-            throws Exception {
-        mNotificationInfo.bindNotification(
-                mMockPackageManager,
-                mMockINotificationManager,
-                mOnUserInteractionCallback,
-                mChannelEditorDialogController,
-                TEST_PACKAGE_NAME,
-                mNotificationChannel,
-                createMultipleChannelSet(MULTIPLE_CHANNEL_COUNT),
-                mEntry,
-                null,
-                null,
-                mUiEventLogger,
-                true,
-                false,
-                true,
-                mAssistantFeedbackController,
-                mMetricsLogger);
-        final TextView channelNameView =
-                mNotificationInfo.findViewById(R.id.channel_name);
-        assertEquals(GONE, channelNameView.getVisibility());
-    }
-
-    @Test
-    @UiThreadTest
-    public void testStopInvisibleIfBundleFromDifferentChannels() throws Exception {
-        mNotificationInfo.bindNotification(
-                mMockPackageManager,
-                mMockINotificationManager,
-                mOnUserInteractionCallback,
-                mChannelEditorDialogController,
-                TEST_PACKAGE_NAME,
-                mNotificationChannel,
-                createMultipleChannelSet(MULTIPLE_CHANNEL_COUNT),
-                mEntry,
-                null,
-                null,
-                mUiEventLogger,
-                true,
-                false,
-                true,
-                mAssistantFeedbackController,
-                mMetricsLogger);
-        assertEquals(GONE, mNotificationInfo.findViewById(
-                R.id.interruptiveness_settings).getVisibility());
-        assertEquals(VISIBLE, mNotificationInfo.findViewById(
-                R.id.non_configurable_multichannel_text).getVisibility());
-    }
-
-    @Test
     public void testBindNotification_whenAppUnblockable() throws Exception {
         mNotificationInfo.bindNotification(
                 mMockPackageManager,
@@ -639,7 +539,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -683,7 +582,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -727,7 +625,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -755,7 +652,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -778,7 +674,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -803,7 +698,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -825,7 +719,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -847,7 +740,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -869,7 +761,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -893,7 +784,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -918,7 +808,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -946,7 +835,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -974,7 +862,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -1003,7 +890,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -1031,7 +917,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -1067,7 +952,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -1096,7 +980,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -1138,7 +1021,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -1176,7 +1058,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -1209,7 +1090,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -1246,7 +1126,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -1285,7 +1164,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -1317,7 +1195,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -1356,7 +1233,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -1386,7 +1262,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -1418,7 +1293,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -1454,7 +1328,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -1488,7 +1361,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -1522,7 +1394,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -1549,7 +1420,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 null,
@@ -1562,21 +1432,4 @@
 
         assertFalse(mNotificationInfo.willBeRemoved());
     }
-
-    private Set<NotificationChannel> createMultipleChannelSet(int howMany) {
-        Set<NotificationChannel> multiChannelSet = new HashSet<>();
-        for (int i = 0; i < howMany; i++) {
-            if (i == 0) {
-                multiChannelSet.add(mNotificationChannel);
-                continue;
-            }
-
-            NotificationChannel channel = new NotificationChannel(
-                    TEST_CHANNEL, TEST_CHANNEL_NAME + i, IMPORTANCE_LOW);
-
-            multiChannelSet.add(channel);
-        }
-
-        return multiChannelSet;
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/PartialConversationInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/PartialConversationInfoTest.java
index e42ce27..ccedd36 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/PartialConversationInfoTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/PartialConversationInfoTest.java
@@ -83,8 +83,6 @@
     private PartialConversationInfo mInfo;
     private NotificationChannel mNotificationChannel;
     private NotificationChannel mDefaultNotificationChannel;
-    private Set<NotificationChannel> mNotificationChannelSet = new HashSet<>();
-    private Set<NotificationChannel> mDefaultNotificationChannelSet = new HashSet<>();
     private StatusBarNotification mSbn;
     private NotificationEntry mEntry;
 
@@ -144,11 +142,9 @@
         // Some test channels.
         mNotificationChannel = new NotificationChannel(
                 TEST_CHANNEL, TEST_CHANNEL_NAME, IMPORTANCE_LOW);
-        mNotificationChannelSet.add(mNotificationChannel);
         mDefaultNotificationChannel = new NotificationChannel(
                 NotificationChannel.DEFAULT_CHANNEL_ID, TEST_CHANNEL_NAME,
                 IMPORTANCE_LOW);
-        mDefaultNotificationChannelSet.add(mDefaultNotificationChannel);
         Notification n = new Notification.Builder(mContext, mNotificationChannel.getId())
                 .setContentTitle(new SpannableString("title"))
                 .build();
@@ -166,7 +162,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 true,
@@ -185,7 +180,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 true,
@@ -202,7 +196,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 true,
@@ -228,7 +221,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 entry,
                 null,
                 true,
@@ -247,7 +239,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 (View v, NotificationChannel c, int appUid) -> {
                     assertEquals(mNotificationChannel, c);
@@ -271,7 +262,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 (View v, NotificationChannel c, int appUid) -> {
                     assertEquals(mNotificationChannel, c);
@@ -294,7 +284,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 true,
@@ -311,7 +300,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 (View v, NotificationChannel c, int appUid) -> {
                     assertEquals(mNotificationChannel, c);
@@ -330,7 +318,6 @@
                 mChannelEditorDialogController,
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
-                mNotificationChannelSet,
                 mEntry,
                 null,
                 true,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 5c3dde5..0b171b2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -364,7 +364,8 @@
                         mock(NotifPipelineFlags.class),
                         mock(KeyguardNotificationVisibilityProvider.class),
                         mock(UiEventLogger.class),
-                        mUserTracker);
+                        mUserTracker,
+                        mDeviceProvisionedController);
 
         mContext.addMockSystemService(TrustManager.class, mock(TrustManager.class));
         mContext.addMockSystemService(FingerprintManager.class, mock(FingerprintManager.class));
@@ -1169,7 +1170,8 @@
                 NotifPipelineFlags flags,
                 KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
                 UiEventLogger uiEventLogger,
-                UserTracker userTracker) {
+                UserTracker userTracker,
+                DeviceProvisionedController deviceProvisionedController) {
             super(
                     contentResolver,
                     powerManager,
@@ -1183,7 +1185,8 @@
                     flags,
                     keyguardNotificationVisibilityProvider,
                     uiEventLogger,
-                    userTracker
+                    userTracker,
+                    deviceProvisionedController
             );
             mUseHeadsUp = true;
         }
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/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerTest.java
index c8f28bc..4ccbd1b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingControllerTest.java
@@ -65,7 +65,7 @@
 
     private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
     private final FakeExecutor mFakeExecutor = new FakeExecutor(mFakeSystemClock);
-    private final RotationPolicyWrapper mFakeRotationPolicy = new FakeRotationPolicy();
+    private final FakeRotationPolicy mFakeRotationPolicy = new FakeRotationPolicy();
     private DeviceStateRotationLockSettingController mDeviceStateRotationLockSettingController;
     private DeviceStateManager.DeviceStateCallback mDeviceStateCallback;
     private DeviceStateRotationLockSettingsManager mSettingsManager;
@@ -324,13 +324,21 @@
 
         private boolean mRotationLock;
 
-        @Override
         public void setRotationLock(boolean enabled) {
-            mRotationLock = enabled;
+            setRotationLock(enabled, /* caller= */ "FakeRotationPolicy");
         }
 
         @Override
+        public void setRotationLock(boolean enabled, String caller) {
+            mRotationLock = enabled;
+        }
+
         public void setRotationLockAtAngle(boolean enabled, int rotation) {
+            setRotationLockAtAngle(enabled, rotation, /* caller= */ "FakeRotationPolicy");
+        }
+
+        @Override
+        public void setRotationLockAtAngle(boolean enabled, int rotation, String caller) {
             mRotationLock = enabled;
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
index bbc49c8..af941d0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
@@ -34,7 +34,6 @@
 import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.systemui.GuestResetOrExitSessionReceiver
 import com.android.systemui.GuestResumeSessionReceiver
-import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.Text
@@ -45,6 +44,7 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.qs.user.UserSwitchDialogController
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
 import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
 import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
@@ -1120,6 +1120,7 @@
                     ),
                 uiEventLogger = uiEventLogger,
                 featureFlags = featureFlags,
+                userRestrictionChecker = mock(),
             )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
index 2433e12..a8db368 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
@@ -267,6 +267,7 @@
                     refreshUsersScheduler = refreshUsersScheduler,
                     guestUserInteractor = guestUserInteractor,
                     uiEventLogger = uiEventLogger,
+                    userRestrictionChecker = mock(),
                 )
         )
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
index 8c88f95..6932f5e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
@@ -45,6 +45,7 @@
 import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
 import com.android.systemui.user.shared.model.UserActionModel
 import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -176,6 +177,7 @@
                         refreshUsersScheduler = refreshUsersScheduler,
                         guestUserInteractor = guestUserInteractor,
                         uiEventLogger = uiEventLogger,
+                        userRestrictionChecker = mock(),
                     ),
                 guestUserInteractor = guestUserInteractor,
             )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubbleEducationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubbleEducationControllerTest.kt
index 94ed608..e59e475 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubbleEducationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubbleEducationControllerTest.kt
@@ -15,104 +15,119 @@
  */
 package com.android.systemui.wmshell
 
-import android.content.ContentResolver
 import android.content.Context
+import android.content.Intent
 import android.content.SharedPreferences
+import android.content.pm.ShortcutInfo
+import android.content.res.Resources
+import android.os.UserHandle
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
+import androidx.core.content.edit
 import androidx.test.filters.SmallTest
 import com.android.systemui.model.SysUiStateTest
 import com.android.wm.shell.bubbles.Bubble
 import com.android.wm.shell.bubbles.BubbleEducationController
 import com.android.wm.shell.bubbles.PREF_MANAGED_EDUCATION
 import com.android.wm.shell.bubbles.PREF_STACK_EDUCATION
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors.directExecutor
 import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.anyBoolean
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.ArgumentMatchers.anyString
-import org.mockito.Mockito
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 class BubbleEducationControllerTest : SysUiStateTest() {
-    private val sharedPrefsEditor = Mockito.mock(SharedPreferences.Editor::class.java)
-    private val sharedPrefs = Mockito.mock(SharedPreferences::class.java)
-    private val context = Mockito.mock(Context::class.java)
+
+    private lateinit var sharedPrefs: SharedPreferences
     private lateinit var sut: BubbleEducationController
 
     @Before
     fun setUp() {
-        Mockito.`when`(context.packageName).thenReturn("packageName")
-        Mockito.`when`(context.getSharedPreferences(anyString(), anyInt())).thenReturn(sharedPrefs)
-        Mockito.`when`(context.contentResolver)
-            .thenReturn(Mockito.mock(ContentResolver::class.java))
-        Mockito.`when`(sharedPrefs.edit()).thenReturn(sharedPrefsEditor)
-        sut = BubbleEducationController(context)
+        sharedPrefs = mContext.getSharedPreferences(mContext.packageName, Context.MODE_PRIVATE)
+        sharedPrefs.edit {
+            remove(PREF_STACK_EDUCATION)
+            remove(PREF_MANAGED_EDUCATION)
+        }
+        sut = BubbleEducationController(mContext)
     }
 
     @Test
     fun testSeenStackEducation_read() {
-        Mockito.`when`(sharedPrefs.getBoolean(anyString(), anyBoolean())).thenReturn(true)
+        sharedPrefs.edit { putBoolean(PREF_STACK_EDUCATION, true) }
         assertEquals(sut.hasSeenStackEducation, true)
-        Mockito.verify(sharedPrefs).getBoolean(PREF_STACK_EDUCATION, false)
     }
 
     @Test
     fun testSeenStackEducation_write() {
         sut.hasSeenStackEducation = true
-        Mockito.verify(sharedPrefsEditor).putBoolean(PREF_STACK_EDUCATION, true)
+        assertThat(sharedPrefs.getBoolean(PREF_STACK_EDUCATION, false)).isTrue()
     }
 
     @Test
     fun testSeenManageEducation_read() {
-        Mockito.`when`(sharedPrefs.getBoolean(anyString(), anyBoolean())).thenReturn(true)
+        sharedPrefs.edit { putBoolean(PREF_MANAGED_EDUCATION, true) }
         assertEquals(sut.hasSeenManageEducation, true)
-        Mockito.verify(sharedPrefs).getBoolean(PREF_MANAGED_EDUCATION, false)
     }
 
     @Test
     fun testSeenManageEducation_write() {
         sut.hasSeenManageEducation = true
-        Mockito.verify(sharedPrefsEditor).putBoolean(PREF_MANAGED_EDUCATION, true)
+        assertThat(sharedPrefs.getBoolean(PREF_MANAGED_EDUCATION, false)).isTrue()
     }
 
     @Test
     fun testShouldShowStackEducation() {
-        val bubble = Mockito.mock(Bubble::class.java)
         // When bubble is null
         assertEquals(sut.shouldShowStackEducation(null), false)
+        var bubble = createFakeBubble(isConversational = false)
         // When bubble is not conversation
-        Mockito.`when`(bubble.isConversation).thenReturn(false)
         assertEquals(sut.shouldShowStackEducation(bubble), false)
         // When bubble is conversation and has seen stack edu
-        Mockito.`when`(bubble.isConversation).thenReturn(true)
-        Mockito.`when`(sharedPrefs.getBoolean(anyString(), anyBoolean())).thenReturn(true)
+        bubble = createFakeBubble(isConversational = true)
+        sharedPrefs.edit { putBoolean(PREF_STACK_EDUCATION, true) }
         assertEquals(sut.shouldShowStackEducation(bubble), false)
         // When bubble is conversation and has not seen stack edu
-        Mockito.`when`(bubble.isConversation).thenReturn(true)
-        Mockito.`when`(sharedPrefs.getBoolean(anyString(), anyBoolean())).thenReturn(false)
+        sharedPrefs.edit { remove(PREF_STACK_EDUCATION) }
         assertEquals(sut.shouldShowStackEducation(bubble), true)
     }
 
     @Test
     fun testShouldShowManageEducation() {
-        val bubble = Mockito.mock(Bubble::class.java)
         // When bubble is null
         assertEquals(sut.shouldShowManageEducation(null), false)
+        var bubble = createFakeBubble(isConversational = false)
         // When bubble is not conversation
-        Mockito.`when`(bubble.isConversation).thenReturn(false)
         assertEquals(sut.shouldShowManageEducation(bubble), false)
         // When bubble is conversation and has seen stack edu
-        Mockito.`when`(bubble.isConversation).thenReturn(true)
-        Mockito.`when`(sharedPrefs.getBoolean(anyString(), anyBoolean())).thenReturn(true)
+        bubble = createFakeBubble(isConversational = true)
+        sharedPrefs.edit { putBoolean(PREF_MANAGED_EDUCATION, true) }
         assertEquals(sut.shouldShowManageEducation(bubble), false)
         // When bubble is conversation and has not seen stack edu
-        Mockito.`when`(bubble.isConversation).thenReturn(true)
-        Mockito.`when`(sharedPrefs.getBoolean(anyString(), anyBoolean())).thenReturn(false)
+        sharedPrefs.edit { remove(PREF_MANAGED_EDUCATION) }
         assertEquals(sut.shouldShowManageEducation(bubble), true)
     }
+
+    private fun createFakeBubble(isConversational: Boolean): Bubble {
+        return if (isConversational) {
+            val shortcutInfo = ShortcutInfo.Builder(mContext, "fakeId").build()
+            Bubble(
+                "key",
+                shortcutInfo,
+                /* desiredHeight= */ 6,
+                Resources.ID_NULL,
+                "title",
+                /* taskId= */ 0,
+                "locus",
+                /* isDismissable= */ true,
+                directExecutor()
+            ) {}
+        } else {
+            val intent = Intent(Intent.ACTION_VIEW).setPackage(mContext.packageName)
+            Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index d8511e8..65b8b55 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -126,6 +126,7 @@
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.ZenModeController;
@@ -391,7 +392,8 @@
                         mock(NotifPipelineFlags.class),
                         mock(KeyguardNotificationVisibilityProvider.class),
                         mock(UiEventLogger.class),
-                        mock(UserTracker.class)
+                        mock(UserTracker.class),
+                        mock(DeviceProvisionedController.class)
                 );
 
         mShellTaskOrganizer = new ShellTaskOrganizer(mock(ShellInit.class),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
index 4e14bbf6..0df235d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
@@ -29,6 +29,7 @@
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptLogger;
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl;
 import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
@@ -48,7 +49,8 @@
             NotifPipelineFlags flags,
             KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
             UiEventLogger uiEventLogger,
-            UserTracker userTracker) {
+            UserTracker userTracker,
+            DeviceProvisionedController deviceProvisionedController) {
         super(contentResolver,
                 powerManager,
                 ambientDisplayConfiguration,
@@ -61,7 +63,8 @@
                 flags,
                 keyguardNotificationVisibilityProvider,
                 uiEventLogger,
-                userTracker);
+                userTracker,
+                deviceProvisionedController);
         mUseHeadsUp = true;
     }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/FakeSystemUiModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/FakeSystemUiModule.kt
new file mode 100644
index 0000000..0e59496
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/FakeSystemUiModule.kt
@@ -0,0 +1,37 @@
+/*
+ * 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
+
+import com.android.systemui.data.FakeSystemUiDataLayerModule
+import com.android.systemui.flags.FakeFeatureFlagsClassicModule
+import com.android.systemui.log.FakeUiEventLoggerModule
+import com.android.systemui.scene.FakeSceneModule
+import com.android.systemui.settings.FakeSettingsModule
+import com.android.systemui.util.concurrency.FakeExecutorModule
+import dagger.Module
+
+@Module(
+    includes =
+        [
+            FakeExecutorModule::class,
+            FakeFeatureFlagsClassicModule::class,
+            FakeSettingsModule::class,
+            FakeSceneModule::class,
+            FakeSystemUiDataLayerModule::class,
+            FakeUiEventLoggerModule::class,
+        ]
+)
+object FakeSystemUiModule
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
index aa88a46..cd009df 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
@@ -193,7 +193,7 @@
         return null;
     }
 
-    protected FakeBroadcastDispatcher getFakeBroadcastDispatcher() {
+    public FakeBroadcastDispatcher getFakeBroadcastDispatcher() {
         return mFakeBroadcastDispatcher;
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeBouncerDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeBouncerDataLayerModule.kt
new file mode 100644
index 0000000..42c02c4
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeBouncerDataLayerModule.kt
@@ -0,0 +1,20 @@
+/*
+ * 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.bouncer.data.repository
+
+import dagger.Module
+
+@Module(includes = [FakeKeyguardBouncerRepositoryModule::class]) object FakeBouncerDataLayerModule
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeKeyguardBouncerRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeKeyguardBouncerRepository.kt
index b45c198..f84481c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeKeyguardBouncerRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeKeyguardBouncerRepository.kt
@@ -2,6 +2,10 @@
 
 import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants
 import com.android.systemui.bouncer.shared.model.BouncerShowMessageModel
+import com.android.systemui.dagger.SysUISingleton
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -10,7 +14,8 @@
 import kotlinx.coroutines.flow.asStateFlow
 
 /** Fake implementation of [KeyguardBouncerRepository] */
-class FakeKeyguardBouncerRepository : KeyguardBouncerRepository {
+@SysUISingleton
+class FakeKeyguardBouncerRepository @Inject constructor() : KeyguardBouncerRepository {
     private val _primaryBouncerShow = MutableStateFlow(false)
     override val primaryBouncerShow = _primaryBouncerShow.asStateFlow()
     private val _primaryBouncerShowingSoon = MutableStateFlow(false)
@@ -112,3 +117,8 @@
         _sideFpsShowing.value = isShowing
     }
 }
+
+@Module
+interface FakeKeyguardBouncerRepositoryModule {
+    @Binds fun bindFake(fake: FakeKeyguardBouncerRepository): KeyguardBouncerRepository
+}
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/common/ui/data/FakeCommonDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/FakeCommonDataLayerModule.kt
new file mode 100644
index 0000000..1f4cb65
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/FakeCommonDataLayerModule.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.common.ui.data
+
+import com.android.systemui.common.ui.data.repository.FakeConfigurationRepositoryModule
+import dagger.Module
+
+@Module(includes = [FakeConfigurationRepositoryModule::class]) object FakeCommonDataLayerModule
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt
index 72cdbbc..10b284a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt
@@ -16,13 +16,18 @@
 
 package com.android.systemui.common.ui.data.repository
 
+import com.android.systemui.dagger.SysUISingleton
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.asStateFlow
 
-class FakeConfigurationRepository : ConfigurationRepository {
+@SysUISingleton
+class FakeConfigurationRepository @Inject constructor() : ConfigurationRepository {
     private val _onAnyConfigurationChange = MutableSharedFlow<Unit>()
     override val onAnyConfigurationChange: Flow<Unit> = _onAnyConfigurationChange.asSharedFlow()
 
@@ -45,3 +50,8 @@
         throw IllegalStateException("Don't use for tests")
     }
 }
+
+@Module
+interface FakeConfigurationRepositoryModule {
+    @Binds fun bindFake(fake: FakeConfigurationRepository): ConfigurationRepository
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/data/FakeSystemUiDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/data/FakeSystemUiDataLayerModule.kt
new file mode 100644
index 0000000..f866932
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/data/FakeSystemUiDataLayerModule.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.data
+
+import com.android.systemui.bouncer.data.repository.FakeBouncerDataLayerModule
+import com.android.systemui.common.ui.data.FakeCommonDataLayerModule
+import com.android.systemui.keyguard.data.FakeKeyguardDataLayerModule
+import com.android.systemui.power.data.FakePowerDataLayerModule
+import com.android.systemui.shade.data.repository.FakeShadeDataLayerModule
+import com.android.systemui.statusbar.data.FakeStatusBarDataLayerModule
+import com.android.systemui.telephony.data.FakeTelephonyDataLayerModule
+import com.android.systemui.user.data.FakeUserDataLayerModule
+import dagger.Module
+
+@Module(
+    includes =
+        [
+            FakeCommonDataLayerModule::class,
+            FakeBouncerDataLayerModule::class,
+            FakeKeyguardDataLayerModule::class,
+            FakePowerDataLayerModule::class,
+            FakeShadeDataLayerModule::class,
+            FakeStatusBarDataLayerModule::class,
+            FakeTelephonyDataLayerModule::class,
+            FakeUserDataLayerModule::class,
+        ]
+)
+object FakeSystemUiDataLayerModule
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
index 43c9c99..32469b6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
@@ -16,6 +16,9 @@
 
 package com.android.systemui.flags
 
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
 import java.io.PrintWriter
 
 class FakeFeatureFlagsClassic : FakeFeatureFlags()
@@ -159,3 +162,20 @@
             ?: error("Flag ${flagName(flagName)} was accessed as int but not specified.")
     }
 }
+
+@Module(includes = [FakeFeatureFlagsClassicModule.Bindings::class])
+class FakeFeatureFlagsClassicModule(
+    @get:Provides val fakeFeatureFlagsClassic: FakeFeatureFlagsClassic = FakeFeatureFlagsClassic(),
+) {
+
+    constructor(
+        block: FakeFeatureFlagsClassic.() -> Unit
+    ) : this(FakeFeatureFlagsClassic().apply(block))
+
+    @Module
+    interface Bindings {
+        @Binds fun bindFake(fake: FakeFeatureFlagsClassic): FeatureFlagsClassic
+        @Binds fun bindClassic(classic: FeatureFlagsClassic): FeatureFlags
+        @Binds fun bindFakeClassic(fake: FakeFeatureFlagsClassic): FakeFeatureFlags
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/FakeKeyguardDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/FakeKeyguardDataLayerModule.kt
new file mode 100644
index 0000000..abf72af
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/FakeKeyguardDataLayerModule.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.keyguard.data
+
+import com.android.systemui.keyguard.data.repository.FakeCommandQueueModule
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepositoryModule
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepositoryModule
+import dagger.Module
+
+@Module(
+    includes =
+        [
+            FakeCommandQueueModule::class,
+            FakeKeyguardRepositoryModule::class,
+            FakeKeyguardTransitionRepositoryModule::class,
+        ]
+)
+object FakeKeyguardDataLayerModule
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeCommandQueue.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeCommandQueue.kt
index fe94117..3a59f6a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeCommandQueue.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeCommandQueue.kt
@@ -18,11 +18,17 @@
 package com.android.systemui.keyguard.data.repository
 
 import android.content.Context
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.settings.DisplayTracker
 import com.android.systemui.statusbar.CommandQueue
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
 import org.mockito.Mockito.mock
 
-class FakeCommandQueue : CommandQueue(mock(Context::class.java), mock(DisplayTracker::class.java)) {
+@SysUISingleton
+class FakeCommandQueue @Inject constructor() :
+    CommandQueue(mock(Context::class.java), mock(DisplayTracker::class.java)) {
     private val callbacks = mutableListOf<Callbacks>()
 
     override fun addCallback(callback: Callbacks) {
@@ -39,3 +45,8 @@
 
     fun callbackCount(): Int = callbacks.size
 }
+
+@Module
+interface FakeCommandQueueModule {
+    @Binds fun bindFake(fake: FakeCommandQueue): CommandQueue
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index dae8644..a5f5d52 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -19,6 +19,7 @@
 
 import android.graphics.Point
 import com.android.systemui.common.shared.model.Position
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
 import com.android.systemui.keyguard.shared.model.DismissAction
@@ -31,6 +32,9 @@
 import com.android.systemui.keyguard.shared.model.WakeSleepReason
 import com.android.systemui.keyguard.shared.model.WakefulnessModel
 import com.android.systemui.keyguard.shared.model.WakefulnessState
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -38,7 +42,8 @@
 import kotlinx.coroutines.flow.asStateFlow
 
 /** Fake implementation of [KeyguardRepository] */
-class FakeKeyguardRepository : KeyguardRepository {
+@SysUISingleton
+class FakeKeyguardRepository @Inject constructor() : KeyguardRepository {
     private val _deferKeyguardDone: MutableSharedFlow<KeyguardDone> = MutableSharedFlow()
     override val keyguardDone: Flow<KeyguardDone> = _deferKeyguardDone
 
@@ -280,3 +285,8 @@
             )
     }
 }
+
+@Module
+interface FakeKeyguardRepositoryModule {
+    @Binds fun bindFake(fake: FakeKeyguardRepository): KeyguardRepository
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
index dd513db..e160548 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
@@ -18,16 +18,21 @@
 package com.android.systemui.keyguard.data.repository
 
 import android.annotation.FloatRange
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.shared.model.TransitionInfo
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import dagger.Binds
+import dagger.Module
 import java.util.UUID
+import javax.inject.Inject
 import kotlinx.coroutines.channels.BufferOverflow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.SharedFlow
 
 /** Fake implementation of [KeyguardTransitionRepository] */
-class FakeKeyguardTransitionRepository : KeyguardTransitionRepository {
+@SysUISingleton
+class FakeKeyguardTransitionRepository @Inject constructor() : KeyguardTransitionRepository {
 
     private val _transitions =
         MutableSharedFlow<TransitionStep>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
@@ -47,3 +52,8 @@
         state: TransitionState
     ) = Unit
 }
+
+@Module
+interface FakeKeyguardTransitionRepositoryModule {
+    @Binds fun bindFake(fake: FakeKeyguardTransitionRepository): KeyguardTransitionRepository
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt
index ceab8e9..945aaed 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt
@@ -50,6 +50,7 @@
 import com.android.systemui.user.domain.interactor.RefreshUsersScheduler
 import com.android.systemui.user.domain.interactor.UserInteractor
 import com.android.systemui.util.time.FakeSystemClock
+import com.android.systemui.utils.UserRestrictionChecker
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.test.TestScope
 import org.mockito.Mockito.mock
@@ -137,6 +138,7 @@
                 refreshUsersScheduler = mock(RefreshUsersScheduler::class.java),
                 guestUserInteractor = mock(GuestUserInteractor::class.java),
                 uiEventLogger = mock(UiEventLogger::class.java),
+                userRestrictionChecker = mock(UserRestrictionChecker::class.java),
             )
         return WithDependencies(
             trustRepository = trustRepository,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/log/FakeUiEventLoggerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/log/FakeUiEventLoggerModule.kt
new file mode 100644
index 0000000..3eb909e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/log/FakeUiEventLoggerModule.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.log
+
+import com.android.internal.logging.UiEventLogger
+import com.android.internal.logging.testing.UiEventLoggerFake
+import com.android.systemui.dagger.SysUISingleton
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+
+@Module
+interface FakeUiEventLoggerModule {
+
+    @Binds fun bindFake(fake: UiEventLoggerFake): UiEventLogger
+
+    @Module
+    companion object {
+        @Provides @SysUISingleton fun provideFake(): UiEventLoggerFake = UiEventLoggerFake()
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/power/data/FakePowerDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/power/data/FakePowerDataLayerModule.kt
new file mode 100644
index 0000000..7cbfa24
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/power/data/FakePowerDataLayerModule.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.power.data
+
+import com.android.systemui.power.data.repository.FakePowerRepositoryModule
+import dagger.Module
+
+@Module(includes = [FakePowerRepositoryModule::class]) object FakePowerDataLayerModule
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/power/data/repository/FakePowerRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/power/data/repository/FakePowerRepository.kt
index b92d946..5ab8204 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/power/data/repository/FakePowerRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/power/data/repository/FakePowerRepository.kt
@@ -18,15 +18,18 @@
 package com.android.systemui.power.data.repository
 
 import android.os.PowerManager
+import com.android.systemui.dagger.SysUISingleton
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
 
-class FakePowerRepository(
-    initialInteractive: Boolean = true,
-) : PowerRepository {
+@SysUISingleton
+class FakePowerRepository @Inject constructor() : PowerRepository {
 
-    private val _isInteractive = MutableStateFlow(initialInteractive)
+    private val _isInteractive = MutableStateFlow(true)
     override val isInteractive: Flow<Boolean> = _isInteractive.asStateFlow()
 
     var lastWakeWhy: String? = null
@@ -47,3 +50,8 @@
         userTouchRegistered = true
     }
 }
+
+@Module
+interface FakePowerRepositoryModule {
+    @Binds fun bindFake(fake: FakePowerRepository): PowerRepository
+}
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/packages/SystemUI/tests/utils/src/com/android/systemui/scene/FakeSceneModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/FakeSceneModule.kt
new file mode 100644
index 0000000..5d22a6e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/FakeSceneModule.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.scene
+
+import com.android.systemui.scene.shared.flag.FakeSceneContainerFlagsModule
+import com.android.systemui.scene.shared.model.FakeSceneContainerConfigModule
+import dagger.Module
+
+@Module(includes = [FakeSceneContainerConfigModule::class, FakeSceneContainerFlagsModule::class])
+object FakeSceneModule
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
index 1620dc27..69c89e8 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
@@ -206,6 +206,7 @@
         return BouncerViewModel(
             applicationContext = context,
             applicationScope = applicationScope(),
+            mainDispatcher = testDispatcher,
             bouncerInteractor = bouncerInteractor,
             authenticationInteractor = authenticationInteractor,
             flags = sceneContainerFlags,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/flag/FakeSceneContainerFlags.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/flag/FakeSceneContainerFlags.kt
index 01a1ece..bae5257 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/flag/FakeSceneContainerFlags.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/flag/FakeSceneContainerFlags.kt
@@ -16,6 +16,10 @@
 
 package com.android.systemui.scene.shared.flag
 
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+
 class FakeSceneContainerFlags(
     var enabled: Boolean = false,
 ) : SceneContainerFlags {
@@ -28,3 +32,13 @@
         return ""
     }
 }
+
+@Module(includes = [FakeSceneContainerFlagsModule.Bindings::class])
+class FakeSceneContainerFlagsModule(
+    @get:Provides val sceneContainerFlags: FakeSceneContainerFlags = FakeSceneContainerFlags(),
+) {
+    @Module
+    interface Bindings {
+        @Binds fun bindFake(fake: FakeSceneContainerFlags): SceneContainerFlags
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneContainerConfigModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneContainerConfigModule.kt
new file mode 100644
index 0000000..b4fc948
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneContainerConfigModule.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.scene.shared.model
+
+import dagger.Module
+import dagger.Provides
+
+@Module
+data class FakeSceneContainerConfigModule(
+    @get:Provides
+    val sceneContainerConfig: SceneContainerConfig =
+        SceneContainerConfig(
+            sceneKeys =
+                listOf(
+                    SceneKey.QuickSettings,
+                    SceneKey.Shade,
+                    SceneKey.Lockscreen,
+                    SceneKey.Bouncer,
+                    SceneKey.Gone,
+                ),
+            initialSceneKey = SceneKey.Lockscreen,
+        ),
+)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeSettingsModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeSettingsModule.kt
new file mode 100644
index 0000000..c9a416e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeSettingsModule.kt
@@ -0,0 +1,20 @@
+/*
+ * 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.settings
+
+import dagger.Module
+
+@Module(includes = [FakeUserTrackerModule::class]) object FakeSettingsModule
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
index f5f924d..4307ff9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
@@ -22,6 +22,9 @@
 import android.os.UserHandle
 import android.test.mock.MockContentResolver
 import com.android.systemui.util.mockito.mock
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
 import java.util.concurrent.Executor
 
 /** A fake [UserTracker] to be used in tests. */
@@ -84,3 +87,13 @@
         callbacks.forEach { it.onProfilesChanged(_userProfiles) }
     }
 }
+
+@Module(includes = [FakeUserTrackerModule.Bindings::class])
+class FakeUserTrackerModule(
+    @get:Provides val fakeUserTracker: FakeUserTracker = FakeUserTracker()
+) {
+    @Module
+    interface Bindings {
+        @Binds fun bindFake(fake: FakeUserTracker): UserTracker
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeDataLayerModule.kt
new file mode 100644
index 0000000..d90e2ea
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeDataLayerModule.kt
@@ -0,0 +1,20 @@
+/*
+ * 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.shade.data.repository
+
+import dagger.Module
+
+@Module(includes = [FakeShadeRepositoryModule::class]) object FakeShadeDataLayerModule
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
index 8b721b2..3c49c58 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
@@ -17,12 +17,17 @@
 
 package com.android.systemui.shade.data.repository
 
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.shade.domain.model.ShadeModel
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 
 /** Fake implementation of [ShadeRepository] */
-class FakeShadeRepository : ShadeRepository {
+@SysUISingleton
+class FakeShadeRepository @Inject constructor() : ShadeRepository {
 
     private val _shadeModel = MutableStateFlow(ShadeModel())
     override val shadeModel: Flow<ShadeModel> = _shadeModel
@@ -89,3 +94,8 @@
         _legacyShadeExpansion.value = expandedFraction
     }
 }
+
+@Module
+interface FakeShadeRepositoryModule {
+    @Binds fun bindFake(fake: FakeShadeRepository): ShadeRepository
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/FakeStatusBarDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/FakeStatusBarDataLayerModule.kt
new file mode 100644
index 0000000..1bec82b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/FakeStatusBarDataLayerModule.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.data
+
+import com.android.systemui.statusbar.disableflags.data.FakeStatusBarDisableFlagsDataLayerModule
+import com.android.systemui.statusbar.pipeline.data.FakeStatusBarPipelineDataLayerModule
+import dagger.Module
+
+@Module(
+    includes =
+        [
+            FakeStatusBarDisableFlagsDataLayerModule::class,
+            FakeStatusBarPipelineDataLayerModule::class,
+        ]
+)
+object FakeStatusBarDataLayerModule
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/disableflags/data/FakeStatusBarDisableFlagsDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/disableflags/data/FakeStatusBarDisableFlagsDataLayerModule.kt
new file mode 100644
index 0000000..1ffb1b9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/disableflags/data/FakeStatusBarDisableFlagsDataLayerModule.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.disableflags.data
+
+import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepositoryModule
+import dagger.Module
+
+@Module(includes = [FakeDisableFlagsRepositoryModule::class])
+object FakeStatusBarDisableFlagsDataLayerModule
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/disableflags/data/repository/FakeDisableFlagsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/disableflags/data/repository/FakeDisableFlagsRepository.kt
similarity index 71%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/disableflags/data/repository/FakeDisableFlagsRepository.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/disableflags/data/repository/FakeDisableFlagsRepository.kt
index b66231c..466a3eb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/disableflags/data/repository/FakeDisableFlagsRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/disableflags/data/repository/FakeDisableFlagsRepository.kt
@@ -14,9 +14,19 @@
 
 package com.android.systemui.statusbar.disableflags.data.repository
 
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
 
-class FakeDisableFlagsRepository : DisableFlagsRepository {
+@SysUISingleton
+class FakeDisableFlagsRepository @Inject constructor() : DisableFlagsRepository {
     override val disableFlags = MutableStateFlow(DisableFlagsModel())
 }
+
+@Module
+interface FakeDisableFlagsRepositoryModule {
+    @Binds fun bindFake(fake: FakeDisableFlagsRepository): DisableFlagsRepository
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/data/FakeStatusBarPipelineDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/data/FakeStatusBarPipelineDataLayerModule.kt
new file mode 100644
index 0000000..21a52a6
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/data/FakeStatusBarPipelineDataLayerModule.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.pipeline.data
+
+import com.android.systemui.statusbar.pipeline.mobile.data.FakeStatusBarPipelineMobileDataLayerModule
+import dagger.Module
+
+@Module(includes = [FakeStatusBarPipelineMobileDataLayerModule::class])
+object FakeStatusBarPipelineDataLayerModule
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/FakeStatusBarPipelineMobileDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/FakeStatusBarPipelineMobileDataLayerModule.kt
new file mode 100644
index 0000000..549929c
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/FakeStatusBarPipelineMobileDataLayerModule.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.pipeline.mobile.data
+
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepositoryModule
+import dagger.Module
+
+@Module(includes = [FakeUserSetupRepositoryModule::class])
+object FakeStatusBarPipelineMobileDataLayerModule
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt
similarity index 74%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt
index 141b50c..55e81bb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt
@@ -16,10 +16,15 @@
 
 package com.android.systemui.statusbar.pipeline.mobile.data.repository
 
+import com.android.systemui.dagger.SysUISingleton
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
 
 /** Defaults to `true` */
-class FakeUserSetupRepository : UserSetupRepository {
+@SysUISingleton
+class FakeUserSetupRepository @Inject constructor() : UserSetupRepository {
     private val _isUserSetup: MutableStateFlow<Boolean> = MutableStateFlow(true)
     override val isUserSetupFlow = _isUserSetup
 
@@ -27,3 +32,8 @@
         _isUserSetup.value = setup
     }
 }
+
+@Module
+interface FakeUserSetupRepositoryModule {
+    @Binds fun bindFake(fake: FakeUserSetupRepository): UserSetupRepository
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/FakeTelephonyDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/FakeTelephonyDataLayerModule.kt
new file mode 100644
index 0000000..ec866ae
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/FakeTelephonyDataLayerModule.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.telephony.data
+
+import com.android.systemui.telephony.data.repository.FakeTelephonyRepositoryModule
+import dagger.Module
+
+@Module(includes = [FakeTelephonyRepositoryModule::class]) object FakeTelephonyDataLayerModule
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt
index 59f24ef..7c70846 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt
@@ -17,11 +17,16 @@
 
 package com.android.systemui.telephony.data.repository
 
+import com.android.systemui.dagger.SysUISingleton
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
 
-class FakeTelephonyRepository : TelephonyRepository {
+@SysUISingleton
+class FakeTelephonyRepository @Inject constructor() : TelephonyRepository {
 
     private val _callState = MutableStateFlow(0)
     override val callState: Flow<Int> = _callState.asStateFlow()
@@ -30,3 +35,8 @@
         _callState.value = value
     }
 }
+
+@Module
+interface FakeTelephonyRepositoryModule {
+    @Binds fun bindFake(fake: FakeTelephonyRepository): TelephonyRepository
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/FakeUserDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/FakeUserDataLayerModule.kt
new file mode 100644
index 0000000..b00f8d2
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/FakeUserDataLayerModule.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.user.data
+
+import com.android.systemui.user.data.repository.FakeUserRepositoryModule
+import dagger.Module
+
+@Module(includes = [FakeUserRepositoryModule::class]) object FakeUserDataLayerModule
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
index 5ad19ee..1124425 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
@@ -19,17 +19,22 @@
 
 import android.content.pm.UserInfo
 import android.os.UserHandle
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.user.data.model.SelectedUserModel
 import com.android.systemui.user.data.model.SelectionStatus
 import com.android.systemui.user.data.model.UserSwitcherSettingsModel
+import dagger.Binds
+import dagger.Module
 import java.util.concurrent.atomic.AtomicBoolean
+import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.yield
 
-class FakeUserRepository : UserRepository {
+@SysUISingleton
+class FakeUserRepository @Inject constructor() : UserRepository {
     companion object {
         // User id to represent a non system (human) user id. We presume this is the main user.
         private const val MAIN_USER_ID = 10
@@ -117,3 +122,8 @@
         _isGuestUserAutoCreated = value
     }
 }
+
+@Module
+interface FakeUserRepositoryModule {
+    @Binds fun bindFake(fake: FakeUserRepository): UserRepository
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/FakeExecutorModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/FakeExecutorModule.kt
new file mode 100644
index 0000000..5de05c2
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/FakeExecutorModule.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.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.util.concurrency
+
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.util.time.FakeSystemClock
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import java.util.concurrent.Executor
+
+@Module(includes = [FakeExecutorModule.Bindings::class])
+class FakeExecutorModule(
+    @get:Provides val clock: FakeSystemClock = FakeSystemClock(),
+) {
+    @get:Provides val executor = FakeExecutor(clock)
+
+    @Module
+    interface Bindings {
+        @Binds @Main fun bindMainExecutor(executor: FakeExecutor): Executor
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeRotationLockController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeRotationLockController.java
index 4f9cb35..be57658 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeRotationLockController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeRotationLockController.java
@@ -46,7 +46,7 @@
     }
 
     @Override
-    public void setRotationLocked(boolean locked) {
+    public void setRotationLocked(boolean locked, String caller) {
 
     }
 
@@ -56,7 +56,7 @@
     }
 
     @Override
-    public void setRotationLockedAtAngle(boolean locked, int rotation) {
+    public void setRotationLockedAtAngle(boolean locked, int rotation, String caller) {
 
     }
 }
diff --git a/services/Android.bp b/services/Android.bp
index f237095..3ae9360 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -277,4 +277,5 @@
             tag: ".removed-api.txt",
         },
     ],
+    api_surface: "system-server",
 }
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index 0480c22..11189cf 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -25,5 +25,12 @@
     name: "send_a11y_events_based_on_state"
     namespace: "accessibility"
     description: "Sends accessibility events in TouchExplorer#onAccessibilityEvent based on internal state to keep it consistent. This reduces test flakiness."
-bug: "295575684"
-}
\ No newline at end of file
+    bug: "295575684"
+}
+
+flag {
+    name: "add_window_token_without_lock"
+    namespace: "accessibility"
+    description: "Calls WMS.addWindowToken without holding A11yManagerService#mLock"
+    bug: "297972548"
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
index 05b6eb4..fa73cff 100644
--- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
@@ -1506,11 +1506,17 @@
         }
     }
 
-    public void onAdded() {
+    /**
+     * Called when the connection is first created. Add a window token for all known displays.
+     * <p>
+     * <strong>Note:</strong> Should not be called while holding the AccessibilityManagerService
+     * lock because this calls out to WindowManagerService.
+     */
+    void addWindowTokensForAllDisplays() {
         final Display[] displays = mDisplayManager.getDisplays();
         for (int i = 0; i < displays.length; i++) {
             final int displayId = displays[i].getDisplayId();
-            onDisplayAdded(displayId);
+            addWindowTokenForDisplay(displayId);
         }
     }
 
@@ -1518,9 +1524,13 @@
      * Called whenever a logical display has been added to the system. Add a window token for adding
      * an accessibility overlay.
      *
+     * <p>
+     * <strong>Note:</strong> Should not be called while holding the AccessibilityManagerService
+     * lock because this calls out to WindowManagerService.
+     *
      * @param displayId The id of the logical display that was added.
      */
-    public void onDisplayAdded(int displayId) {
+    void addWindowTokenForDisplay(int displayId) {
         final long identity = Binder.clearCallingIdentity();
         try {
             final IBinder overlayWindowToken = new Binder();
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 93ba362..60d4ee6 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -154,6 +154,7 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.IntPair;
+import com.android.internal.util.Preconditions;
 import com.android.server.AccessibilityManagerInternal;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
@@ -4500,6 +4501,20 @@
         private int mSystemUiUid = 0;
 
         AccessibilityDisplayListener(Context context, Handler handler) {
+            if (Flags.addWindowTokenWithoutLock()) {
+                // Avoid concerns about one thread adding displays while another thread removes
+                // them by ensuring the looper is the main looper and the DisplayListener
+                // callbacks are always executed on the one main thread.
+                final boolean isMainHandler = handler.getLooper() == Looper.getMainLooper();
+                final String errorMessage =
+                        "AccessibilityDisplayListener must use the main handler";
+                if (Build.IS_USERDEBUG || Build.IS_ENG) {
+                    Preconditions.checkArgument(isMainHandler, errorMessage);
+                } else if (!isMainHandler) {
+                    Slog.e(LOG_TAG, errorMessage);
+                }
+            }
+
             mDisplayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
             mDisplayManager.registerDisplayListener(this, handler);
             initializeDisplayList();
@@ -4541,11 +4556,21 @@
 
         @Override
         public void onDisplayAdded(int displayId) {
+            if (Flags.addWindowTokenWithoutLock()) {
+                final boolean isMainThread = Looper.getMainLooper().isCurrentThread();
+                final String errorMessage = "onDisplayAdded must be called from the main thread";
+                if (Build.IS_USERDEBUG || Build.IS_ENG) {
+                    Preconditions.checkArgument(isMainThread, errorMessage);
+                } else if (!isMainThread) {
+                    Slog.e(LOG_TAG, errorMessage);
+                }
+            }
             final Display display = mDisplayManager.getDisplay(displayId);
             if (!isValidDisplay(display)) {
                 return;
             }
 
+            final List<AccessibilityServiceConnection> services;
             synchronized (mLock) {
                 mDisplaysList.add(display);
                 mA11yOverlayLayers.put(
@@ -4554,21 +4579,42 @@
                     mInputFilter.onDisplayAdded(display);
                 }
                 AccessibilityUserState userState = getCurrentUserStateLocked();
-                if (displayId != Display.DEFAULT_DISPLAY) {
-                    final List<AccessibilityServiceConnection> services = userState.mBoundServices;
-                    for (int i = 0; i < services.size(); i++) {
-                        AccessibilityServiceConnection boundClient = services.get(i);
-                        boundClient.onDisplayAdded(displayId);
+                if (Flags.addWindowTokenWithoutLock()) {
+                    services = new ArrayList<>(userState.mBoundServices);
+                } else {
+                    services = userState.mBoundServices;
+                    if (displayId != Display.DEFAULT_DISPLAY) {
+                        for (int i = 0; i < services.size(); i++) {
+                            AccessibilityServiceConnection boundClient = services.get(i);
+                            boundClient.addWindowTokenForDisplay(displayId);
+                        }
                     }
                 }
                 updateMagnificationLocked(userState);
                 updateWindowsForAccessibilityCallbackLocked(userState);
                 notifyClearAccessibilityCacheLocked();
             }
+            if (Flags.addWindowTokenWithoutLock()) {
+                if (displayId != Display.DEFAULT_DISPLAY) {
+                    for (int i = 0; i < services.size(); i++) {
+                        AccessibilityServiceConnection boundClient = services.get(i);
+                        boundClient.addWindowTokenForDisplay(displayId);
+                    }
+                }
+            }
         }
 
         @Override
         public void onDisplayRemoved(int displayId) {
+            if (Flags.addWindowTokenWithoutLock()) {
+                final boolean isMainThread = Looper.getMainLooper().isCurrentThread();
+                final String errorMessage = "onDisplayRemoved must be called from the main thread";
+                if (Build.IS_USERDEBUG || Build.IS_ENG) {
+                    Preconditions.checkArgument(isMainThread, errorMessage);
+                } else if (!isMainThread) {
+                    Slog.e(LOG_TAG, errorMessage);
+                }
+            }
             synchronized (mLock) {
                 if (!removeDisplayFromList(displayId)) {
                     return;
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
index 9e70073..7a2a602 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
@@ -44,7 +44,6 @@
 import android.view.Display;
 import android.view.MotionEvent;
 
-
 import com.android.server.inputmethod.InputMethodManagerInternal;
 import com.android.server.wm.ActivityTaskManagerInternal;
 import com.android.server.wm.WindowManagerInternal;
@@ -169,6 +168,10 @@
 
     @Override
     public void onServiceConnected(ComponentName componentName, IBinder service) {
+        AccessibilityUserState userState = mUserStateWeakReference.get();
+        if (userState != null && Flags.addWindowTokenWithoutLock()) {
+            addWindowTokensForAllDisplays();
+        }
         synchronized (mLock) {
             if (mService != service) {
                 if (mService != null) {
@@ -184,7 +187,6 @@
                 }
             }
             mServiceInterface = IAccessibilityServiceClient.Stub.asInterface(service);
-            AccessibilityUserState userState = mUserStateWeakReference.get();
             if (userState == null) return;
             userState.addServiceLocked(this);
             mSystemSupport.onClientChangeLocked(false);
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
index ab6cc71..693526a 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
@@ -224,7 +224,9 @@
 
     void addServiceLocked(AccessibilityServiceConnection serviceConnection) {
         if (!mBoundServices.contains(serviceConnection)) {
-            serviceConnection.onAdded();
+            if (!Flags.addWindowTokenWithoutLock()) {
+                serviceConnection.addWindowTokensForAllDisplays();
+            }
             mBoundServices.add(serviceConnection);
             mComponentNameToServiceMap.put(serviceConnection.getComponentName(), serviceConnection);
             mServiceInfoChangeListener.onServiceInfoChangedLocked(this);
diff --git a/services/accessibility/java/com/android/server/accessibility/UiAutomationManager.java b/services/accessibility/java/com/android/server/accessibility/UiAutomationManager.java
index 208acdf..53c629a 100644
--- a/services/accessibility/java/com/android/server/accessibility/UiAutomationManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/UiAutomationManager.java
@@ -25,9 +25,11 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.os.Binder;
+import android.os.Build;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.IBinder.DeathRecipient;
+import android.os.Looper;
 import android.os.RemoteCallback;
 import android.os.RemoteException;
 import android.util.Slog;
@@ -35,6 +37,7 @@
 import android.view.accessibility.AccessibilityEvent;
 
 import com.android.internal.util.DumpUtils;
+import com.android.internal.util.Preconditions;
 import com.android.server.utils.Slogf;
 import com.android.server.wm.WindowManagerInternal;
 
@@ -98,46 +101,47 @@
         accessibilityServiceInfo.setComponentName(COMPONENT_NAME);
         Slogf.i(LOG_TAG, "Registering UiTestAutomationService (id=%s) when called by user %d",
                 accessibilityServiceInfo.getId(), Binder.getCallingUserHandle().getIdentifier());
-        synchronized (mLock) {
-            if (mUiAutomationService != null) {
-                throw new IllegalStateException(
-                        "UiAutomationService " + mUiAutomationService.mServiceInterface
-                                + "already registered!");
-            }
-
-            try {
-                owner.linkToDeath(mUiAutomationServiceOwnerDeathRecipient, 0);
-            } catch (RemoteException re) {
-                Slog.e(LOG_TAG, "Couldn't register for the death of a UiTestAutomationService!",
-                        re);
-                return;
-            }
-
-            mUiAutomationFlags = flags;
-            mSystemSupport = systemSupport;
-            // Ignore registering UiAutomation if it is not allowed to use the accessibility
-            // subsystem.
-            if (!useAccessibility()) {
-                return;
-            }
-            mUiAutomationService = new UiAutomationService(context, accessibilityServiceInfo, id,
-                    mainHandler, mLock, securityPolicy, systemSupport, trace, windowManagerInternal,
-                    systemActionPerformer, awm);
-            mUiAutomationServiceOwner = owner;
-            mUiAutomationService.mServiceInterface = serviceClient;
-            try {
-                mUiAutomationService.mServiceInterface.asBinder().linkToDeath(mUiAutomationService,
-                        0);
-            } catch (RemoteException re) {
-                Slog.e(LOG_TAG, "Failed registering death link: " + re);
-                destroyUiAutomationService();
-                return;
-            }
-
-            mUiAutomationService.onAdded();
-
-            mUiAutomationService.connectServiceUnknownThread();
+        if (mUiAutomationService != null) {
+            throw new IllegalStateException(
+                    "UiAutomationService " + mUiAutomationService.mServiceInterface
+                            + "already registered!");
         }
+
+        try {
+            owner.linkToDeath(mUiAutomationServiceOwnerDeathRecipient, 0);
+        } catch (RemoteException re) {
+            Slog.e(LOG_TAG, "Couldn't register for the death of a UiTestAutomationService!",
+                    re);
+            return;
+        }
+
+        mUiAutomationFlags = flags;
+        mSystemSupport = systemSupport;
+        // Ignore registering UiAutomation if it is not allowed to use the accessibility
+        // subsystem.
+        if (!useAccessibility()) {
+            return;
+        }
+        mUiAutomationService = new UiAutomationService(context, accessibilityServiceInfo, id,
+                mainHandler, mLock, securityPolicy, systemSupport, trace, windowManagerInternal,
+                systemActionPerformer, awm);
+        mUiAutomationServiceOwner = owner;
+        mUiAutomationService.mServiceInterface = serviceClient;
+        try {
+            mUiAutomationService.mServiceInterface.asBinder().linkToDeath(mUiAutomationService,
+                    0);
+        } catch (RemoteException re) {
+            Slog.e(LOG_TAG, "Failed registering death link: " + re);
+            destroyUiAutomationService();
+            return;
+        }
+
+        if (!Flags.addWindowTokenWithoutLock()) {
+            mUiAutomationService.addWindowTokensForAllDisplays();
+        }
+        // UiAutomationService#connectServiceUnknownThread posts to a handler
+        // so this call should return immediately.
+        mUiAutomationService.connectServiceUnknownThread();
     }
 
     void unregisterUiTestAutomationServiceLocked(IAccessibilityServiceClient serviceClient) {
@@ -253,6 +257,13 @@
             super(context, COMPONENT_NAME, accessibilityServiceInfo, id, mainHandler, lock,
                     securityPolicy, systemSupport, trace, windowManagerInternal,
                     systemActionPerformer, awm);
+            final boolean isMainHandler = mainHandler.getLooper() == Looper.getMainLooper();
+            final String errorMessage = "UiAutomationService must use the main handler";
+            if (Build.IS_USERDEBUG || Build.IS_ENG) {
+                Preconditions.checkArgument(isMainHandler, errorMessage);
+            } else if (!isMainHandler) {
+                Slog.e(LOG_TAG, errorMessage);
+            }
             mMainHandler = mainHandler;
             setDisplayTypes(DISPLAY_TYPE_DEFAULT | DISPLAY_TYPE_PROXY);
         }
@@ -274,6 +285,9 @@
                     // If the serviceInterface is null, the UiAutomation has been shut down on
                     // another thread.
                     if (serviceInterface != null) {
+                        if (Flags.addWindowTokenWithoutLock()) {
+                            mUiAutomationService.addWindowTokensForAllDisplays();
+                        }
                         if (mTrace.isA11yTracingEnabledForTypes(
                                 AccessibilityTrace.FLAGS_ACCESSIBILITY_SERVICE_CLIENT)) {
                             mTrace.logTrace("UiAutomationService.connectServiceUnknownThread",
@@ -286,7 +300,7 @@
                                 mOverlayWindowTokens.get(Display.DEFAULT_DISPLAY));
                     }
                 } catch (RemoteException re) {
-                    Slog.w(LOG_TAG, "Error initialized connection", re);
+                    Slog.w(LOG_TAG, "Error initializing connection", re);
                     destroyUiAutomationService();
                 }
             });
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
index 2ca84f8..30b9d0b 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
@@ -40,6 +40,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.PackageManager;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.Region;
@@ -166,9 +167,12 @@
 
     @VisibleForTesting boolean mIsSinglePanningEnabled;
 
-    /**
-     * FullScreenMagnificationGestureHandler Constructor.
-     */
+    private final FullScreenMagnificationVibrationHelper mFullScreenMagnificationVibrationHelper;
+
+    @VisibleForTesting final OverscrollHandler mOverscrollHandler;
+
+    private final boolean mIsWatch;
+
     public FullScreenMagnificationGestureHandler(@UiContext Context context,
             FullScreenMagnificationController fullScreenMagnificationController,
             AccessibilityTraceManager trace,
@@ -254,11 +258,13 @@
         mDetectingState = new DetectingState(context);
         mViewportDraggingState = new ViewportDraggingState();
         mPanningScalingState = new PanningScalingState(context);
-        mSinglePanningState = new SinglePanningState(context,
-                fullScreenMagnificationVibrationHelper);
+        mSinglePanningState = new SinglePanningState(context);
+        mFullScreenMagnificationVibrationHelper = fullScreenMagnificationVibrationHelper;
         setSinglePanningEnabled(
                 context.getResources()
                         .getBoolean(R.bool.config_enable_a11y_magnification_single_panning));
+        mOverscrollHandler = new OverscrollHandler();
+        mIsWatch = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
 
         if (mDetectShortcutTrigger) {
             mScreenStateReceiver = new ScreenStateReceiver(context, this);
@@ -468,15 +474,21 @@
         @Override
         public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
             int action = event.getActionMasked();
-
             if (action == ACTION_POINTER_UP
                     && event.getPointerCount() == 2 // includes the pointer currently being released
                     && mPreviousState == mViewportDraggingState) {
-
+                // if feature flag is enabled, currently only true on watches
+                if (mIsSinglePanningEnabled) {
+                    mOverscrollHandler.setScaleAndCenterToEdgeIfNeeded();
+                    mOverscrollHandler.clearEdgeState();
+                }
                 persistScaleAndTransitionTo(mViewportDraggingState);
-
             } else if (action == ACTION_UP || action == ACTION_CANCEL) {
-
+                // if feature flag is enabled, currently only true on watches
+                if (mIsSinglePanningEnabled) {
+                    mOverscrollHandler.setScaleAndCenterToEdgeIfNeeded();
+                    mOverscrollHandler.clearEdgeState();
+                }
                 persistScaleAndTransitionTo(mDetectingState);
             }
         }
@@ -502,7 +514,12 @@
         }
 
         public void persistScaleAndTransitionTo(State state) {
-            mFullScreenMagnificationController.persistScale(mDisplayId);
+            // If device is a watch don't change user settings scale. On watches, warp effect
+            // is enabled and the current display scale could be differ from the default user
+            // settings scale (should not change the scale due to the warp effect)
+            if (!mIsWatch) {
+                mFullScreenMagnificationController.persistScale(mDisplayId);
+            }
             clear();
             transitionTo(state);
         }
@@ -546,6 +563,9 @@
             }
             mFullScreenMagnificationController.offsetMagnifiedRegion(mDisplayId, distanceX,
                     distanceY, AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
+            if (mIsSinglePanningEnabled) {
+                mOverscrollHandler.onScrollStateChanged(first, second);
+            }
             return /* event consumed: */ true;
         }
 
@@ -893,6 +913,11 @@
                         if (isMultiTapTriggered(2 /* taps */) && event.getPointerCount() == 1) {
                             transitionToViewportDraggingStateAndClear(event);
                         } else if (isActivated() && event.getPointerCount() == 2) {
+                            if (mIsSinglePanningEnabled
+                                    && overscrollState(event, mFirstPointerDownLocation)
+                                    == OVERSCROLL_VERTICAL_EDGE) {
+                                transitionToDelegatingStateAndClear();
+                            }
                             //Primary pointer is swiping, so transit to PanningScalingState
                             transitToPanningScalingStateAndClear();
                         } else if (mIsSinglePanningEnabled
@@ -1264,6 +1289,7 @@
                 + ", mMagnificationController=" + mFullScreenMagnificationController
                 + ", mDisplayId=" + mDisplayId
                 + ", mIsSinglePanningEnabled=" + mIsSinglePanningEnabled
+                + ", mOverscrollHandler=" + mOverscrollHandler
                 + '}';
     }
 
@@ -1411,32 +1437,8 @@
 
         private final GestureDetector mScrollGestureDetector;
         private MotionEventInfo mEvent;
-        private final FullScreenMagnificationVibrationHelper
-                mFullScreenMagnificationVibrationHelper;
-
-        @VisibleForTesting int mOverscrollState;
-
-        // mPivotEdge is the point on the edge of the screen when the magnified view hits the edge
-        // This point sets the center of magnified view when warp/scale effect is triggered
-        private final PointF mPivotEdge;
-
-        // mReachedEdgeCoord is the user's pointer location on the screen when the magnified view
-        // has hit the edge
-        private final PointF mReachedEdgeCoord;
-        // mEdgeCooldown value will be set to true when user hits the edge and will be set to false
-        // once the user moves x distance away from the edge. This is so that vibrating haptic
-        // doesn't get triggered by slight movements
-        private boolean mEdgeCooldown;
-
-        SinglePanningState(
-                Context context,
-                FullScreenMagnificationVibrationHelper fullScreenMagnificationVibrationHelper) {
+        SinglePanningState(Context context) {
             mScrollGestureDetector = new GestureDetector(context, this, Handler.getMain());
-            mFullScreenMagnificationVibrationHelper = fullScreenMagnificationVibrationHelper;
-            mOverscrollState = OVERSCROLL_NONE;
-            mPivotEdge = new PointF(Float.NaN, Float.NaN);
-            mReachedEdgeCoord = new PointF(Float.NaN, Float.NaN);
-            mEdgeCooldown = false;
         }
 
         @Override
@@ -1445,17 +1447,8 @@
             switch (action) {
                 case ACTION_UP:
                 case ACTION_CANCEL:
-                    if (mOverscrollState == OVERSCROLL_LEFT_EDGE
-                            || mOverscrollState == OVERSCROLL_RIGHT_EDGE) {
-                        mFullScreenMagnificationController.setScaleAndCenter(
-                                mDisplayId,
-                                mFullScreenMagnificationController.getPersistedScale(mDisplayId),
-                                mPivotEdge.x,
-                                mPivotEdge.y,
-                                true,
-                                AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
-                    }
-                    clear();
+                    mOverscrollHandler.setScaleAndCenterToEdgeIfNeeded();
+                    mOverscrollHandler.clearEdgeState();
                     transitionTo(mDetectingState);
                     break;
             }
@@ -1482,23 +1475,7 @@
                                 + " isAtEdge: "
                                 + mFullScreenMagnificationController.isAtEdge(mDisplayId));
             }
-            if (mFullScreenMagnificationController.isAtEdge(mDisplayId)) {
-                playEdgeVibration(second);
-                setPivotEdge();
-            }
-            if (mOverscrollState == OVERSCROLL_NONE) {
-                mOverscrollState = overscrollState(second, new PointF(first.getX(), first.getY()));
-            } else if (mOverscrollState == OVERSCROLL_VERTICAL_EDGE) {
-                clear();
-                transitionTo(mDelegatingState);
-            } else {
-                boolean reset = warpEffectReset(second);
-                if (reset) {
-                    mFullScreenMagnificationController.reset(mDisplayId, /* animate */ true);
-                    clear();
-                    transitionTo(mDetectingState);
-                }
-            }
+            mOverscrollHandler.onScrollStateChanged(first, second);
             return /* event consumed: */ true;
         }
 
@@ -1515,35 +1492,37 @@
         public String toString() {
             return "SinglePanningState{"
                     + "isEdgeOfView="
-                    + mFullScreenMagnificationController.isAtEdge(mDisplayId)
-                    + "overscrollStatus="
-                    + mOverscrollState
-                    + "}";
+                    + mFullScreenMagnificationController.isAtEdge(mDisplayId);
         }
 
-        private void playEdgeVibration(MotionEvent event) {
-            if (mOverscrollState == OVERSCROLL_NONE) {
-                vibrateIfNeeded(event);
-            }
+    }
+
+    /** Overscroll Handler handles the logic when user is at the edge and scrolls past an edge */
+    final class OverscrollHandler {
+
+        @VisibleForTesting int mOverscrollState;
+
+        // mPivotEdge is the point on the edge of the screen when the magnified view hits the edge
+        // This point sets the center of magnified view when warp/scale effect is triggered
+        private final PointF mPivotEdge;
+
+        // mReachedEdgeCoord is the user's pointer location on the screen when the magnified view
+        // has hit the edge
+        private final PointF mReachedEdgeCoord;
+
+        // mEdgeCooldown value will be set to true when user hits the edge and will be set to false
+        // once the user moves x distance away from the edge. This is so that vibrating haptic
+        // doesn't get triggered by slight movements
+        private boolean mEdgeCooldown;
+
+        OverscrollHandler() {
+            mOverscrollState = OVERSCROLL_NONE;
+            mPivotEdge = new PointF(Float.NaN, Float.NaN);
+            mReachedEdgeCoord = new PointF(Float.NaN, Float.NaN);
+            mEdgeCooldown = false;
         }
 
-        private void setPivotEdge() {
-            if (!pointerValid(mPivotEdge)) {
-                Rect bounds = new Rect();
-                mFullScreenMagnificationController.getMagnificationBounds(mDisplayId, bounds);
-                if (mOverscrollState == OVERSCROLL_LEFT_EDGE) {
-                    mPivotEdge.set(
-                            bounds.left,
-                            mFullScreenMagnificationController.getCenterY(mDisplayId));
-                } else if (mOverscrollState == OVERSCROLL_RIGHT_EDGE) {
-                    mPivotEdge.set(
-                            bounds.right,
-                            mFullScreenMagnificationController.getCenterY(mDisplayId));
-                }
-            }
-        }
-
-        private boolean warpEffectReset(MotionEvent second) {
+        protected boolean warpEffectReset(MotionEvent second) {
             float scale = calculateOverscrollScale(second);
             if (scale < 0) return false;
             mFullScreenMagnificationController.setScaleAndCenter(
@@ -1566,7 +1545,7 @@
             float overshootDistX = second.getX() - mReachedEdgeCoord.x;
             if ((mOverscrollState == OVERSCROLL_LEFT_EDGE && overshootDistX < 0)
                     || (mOverscrollState == OVERSCROLL_RIGHT_EDGE && overshootDistX > 0)) {
-                clear();
+                clearEdgeState();
                 return -1.0f;
             }
             float overshootDistY = second.getY() - mReachedEdgeCoord.y;
@@ -1611,21 +1590,109 @@
         }
 
         private void vibrateIfNeeded(MotionEvent event) {
+            if (mOverscrollState != OVERSCROLL_NONE) {
+                return;
+            }
             if ((mFullScreenMagnificationController.isAtLeftEdge(mDisplayId)
                             || mFullScreenMagnificationController.isAtRightEdge(mDisplayId))
                     && !mEdgeCooldown) {
                 mFullScreenMagnificationVibrationHelper.vibrateIfSettingEnabled();
+            }
+        }
+
+        private void setPivotEdge(MotionEvent event) {
+            if (!pointerValid(mPivotEdge)) {
+                Rect bounds = new Rect();
+                mFullScreenMagnificationController.getMagnificationBounds(mDisplayId, bounds);
+                if (mOverscrollState == OVERSCROLL_LEFT_EDGE) {
+                    mPivotEdge.set(
+                            bounds.left, mFullScreenMagnificationController.getCenterY(mDisplayId));
+                } else if (mOverscrollState == OVERSCROLL_RIGHT_EDGE) {
+                    mPivotEdge.set(
+                            bounds.right,
+                            mFullScreenMagnificationController.getCenterY(mDisplayId));
+                }
                 mReachedEdgeCoord.set(event.getX(), event.getY());
                 mEdgeCooldown = true;
             }
         }
 
-        @Override
-        public void clear() {
+        private void onScrollStateChanged(MotionEvent first, MotionEvent second) {
+            if (mFullScreenMagnificationController.isAtEdge(mDisplayId)) {
+                vibrateIfNeeded(second);
+                setPivotEdge(second);
+            }
+            switch (mOverscrollState) {
+                case OVERSCROLL_NONE:
+                    onNoOverscroll(first, second);
+                    break;
+                case OVERSCROLL_VERTICAL_EDGE:
+                    onVerticalOverscroll();
+                    break;
+                case OVERSCROLL_LEFT_EDGE:
+                case OVERSCROLL_RIGHT_EDGE:
+                    onHorizontalOverscroll(second);
+                    break;
+                default:
+                    Slog.d(mLogTag, "Invalid overscroll state");
+                    break;
+            }
+        }
+
+        public void onNoOverscroll(MotionEvent first, MotionEvent second) {
+            mOverscrollState = overscrollState(second, new PointF(first.getX(), first.getY()));
+        }
+
+        public void onVerticalOverscroll() {
+            clearEdgeState();
+            transitionTo(mDelegatingState);
+        }
+
+        public void onHorizontalOverscroll(MotionEvent second) {
+            boolean reset = warpEffectReset(second);
+            if (reset) {
+                mFullScreenMagnificationController.reset(mDisplayId, /* animate */ true);
+                clearEdgeState();
+                transitionTo(mDelegatingState);
+            }
+        }
+
+        private void setScaleAndCenterToEdgeIfNeeded() {
+            if (mOverscrollState == OVERSCROLL_LEFT_EDGE
+                    || mOverscrollState == OVERSCROLL_RIGHT_EDGE) {
+                mFullScreenMagnificationController.setScaleAndCenter(
+                        mDisplayId,
+                        mFullScreenMagnificationController.getPersistedScale(mDisplayId),
+                        mPivotEdge.x,
+                        mPivotEdge.y,
+                        true,
+                        AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
+            }
+        }
+
+        private void clearEdgeState() {
             mOverscrollState = OVERSCROLL_NONE;
             mPivotEdge.set(Float.NaN, Float.NaN);
             mReachedEdgeCoord.set(Float.NaN, Float.NaN);
             mEdgeCooldown = false;
         }
+
+        @Override
+        public String toString() {
+            return "OverscrollHandler {"
+                    + "mOverscrollState="
+                    + mOverscrollState
+                    + "mPivotEdge.x="
+                    + mPivotEdge.x
+                    + "mPivotEdge.y="
+                    + mPivotEdge.y
+                    + "mReachedEdgeCoord.x="
+                    + mReachedEdgeCoord.x
+                    + "mReachedEdgeCoord.y="
+                    + mReachedEdgeCoord.y
+                    + "mEdgeCooldown="
+                    + mEdgeCooldown
+                    + "}";
+        }
     }
 }
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index ba45339..c791498 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -155,6 +155,7 @@
             "debug.cdm.cdmservice.removal_time_window";
 
     private static final long ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT = DAYS.toMillis(90);
+    private static final int MAX_CN_LENGTH = 500;
 
     private final ActivityManager mActivityManager;
     private final OnPackageVisibilityChangeListener mOnPackageVisibilityChangeListener;
@@ -757,6 +758,9 @@
             String callingPackage = component.getPackageName();
             checkCanCallNotificationApi(callingPackage);
             // TODO: check userId.
+            if (component.flattenToString().length() > MAX_CN_LENGTH) {
+                throw new IllegalArgumentException("Component name is too long.");
+            }
             final long identity = Binder.clearCallingIdentity();
             try {
                 return PendingIntent.getActivityAsUser(getContext(),
diff --git a/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java b/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java
index e8839a2..720687e 100644
--- a/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java
+++ b/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java
@@ -562,7 +562,8 @@
     private byte[] constructToken(D2DHandshakeContext.Role role, byte[] authValue)
             throws GeneralSecurityException {
         MessageDigest hash = MessageDigest.getInstance("SHA-256");
-        byte[] roleUtf8 = role.name().getBytes(StandardCharsets.UTF_8);
+        String roleName = role == Role.INITIATOR ? "Initiator" : "Responder";
+        byte[] roleUtf8 = roleName.getBytes(StandardCharsets.UTF_8);
         int tokenLength = roleUtf8.length + authValue.length;
         return hash.digest(ByteBuffer.allocate(tokenLength)
                 .put(roleUtf8)
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/Android.bp b/services/core/Android.bp
index d9c2694..6521fab 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -194,6 +194,7 @@
         "notification_flags_lib",
         "camera_platform_flags_core_java_lib",
         "biometrics_flags_lib",
+        "am_flags_lib",
     ],
     javac_shard_size: 50,
     javacflags: [
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/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java
index c20f0aa..9716cf6 100644
--- a/services/core/java/com/android/server/am/ActivityManagerConstants.java
+++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java
@@ -225,7 +225,7 @@
     /**
      * The default value to {@link #KEY_ENABLE_NEW_OOMADJ}.
      */
-    private static final boolean DEFAULT_ENABLE_NEW_OOM_ADJ = false;
+    private static final boolean DEFAULT_ENABLE_NEW_OOM_ADJ = Flags.oomadjusterCorrectnessRewrite();
 
     /**
      * Same as {@link TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED}
diff --git a/services/core/java/com/android/server/am/Android.bp b/services/core/java/com/android/server/am/Android.bp
new file mode 100644
index 0000000..af1200e
--- /dev/null
+++ b/services/core/java/com/android/server/am/Android.bp
@@ -0,0 +1,10 @@
+aconfig_declarations {
+    name: "am_flags",
+    package: "com.android.server.am",
+    srcs: ["*.aconfig"],
+}
+
+java_aconfig_library {
+    name: "am_flags_lib",
+    aconfig_declarations: "am_flags",
+}
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/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index 5fa0ffa..6005b64 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -1442,7 +1442,6 @@
             uidRec.setFrozen(false);
             postUidFrozenMessage(uidRec.getUid(), false);
         }
-        reportProcessFreezableChangedLocked(app);
 
         opt.setFreezerOverride(false);
         if (pid == 0 || !opt.isFrozen()) {
@@ -1481,6 +1480,7 @@
         if (processKilled) {
             return;
         }
+        reportProcessFreezableChangedLocked(app);
 
         long freezeTime = opt.getFreezeUnfreezeTime();
 
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/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index 3d11c68..0615ecf 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -3087,6 +3087,8 @@
             if (old == proc && proc.isPersistent()) {
                 // We are re-adding a persistent process.  Whatevs!  Just leave it there.
                 Slog.w(TAG, "Re-adding persistent process " + proc);
+                // Ensure that the mCrashing flag is cleared, since this is a restart
+                proc.resetCrashingOnRestart();
             } else if (old != null) {
                 if (old.isKilled()) {
                     // The old process has been killed, we probably haven't had
diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java
index cfbb5a5..d8a2695 100644
--- a/services/core/java/com/android/server/am/ProcessRecord.java
+++ b/services/core/java/com/android/server/am/ProcessRecord.java
@@ -618,6 +618,10 @@
         mPkgList.put(_info.packageName, new ProcessStats.ProcessStateHolder(_info.longVersionCode));
     }
 
+    void resetCrashingOnRestart() {
+        mErrorState.setCrashing(false);
+    }
+
     @GuardedBy(anyOf = {"mService", "mProcLock"})
     UidRecord getUidRecord() {
         return mUidRecord;
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 6d2fc0d..4a0bc4b 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -133,11 +133,14 @@
         "companion",
         "context_hub",
         "core_experiments_team_internal",
+        "core_graphics",
         "haptics",
         "hardware_backed_security_mainline",
+        "machine_learning",
         "media_audio",
         "media_solutions",
         "nfc",
+        "pixel_audio_android",
         "pixel_system_sw_touch",
         "pixel_watch",
         "platform_security",
@@ -151,6 +154,7 @@
         "threadnetwork",
         "tv_system_ui",
         "vibrator",
+        "virtual_devices",
         "wear_frameworks",
         "wear_system_health",
         "wear_systems",
diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig
new file mode 100644
index 0000000..b03cc62
--- /dev/null
+++ b/services/core/java/com/android/server/am/flags.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.server.am"
+
+flag {
+    name: "oomadjuster_correctness_rewrite"
+    namespace: "android_platform_power_optimization"
+    description: "Utilize new OomAdjuster implementation"
+    bug: "298055811"
+    is_fixed_read_only: true
+}
\ No newline at end of file
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/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index 507ae26..9e92c8d 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -500,6 +500,8 @@
 
     public static final String DEFAULT_ID = "default";
 
+    public static final int DEFAULT_LOW_REFRESH_RATE = 60;
+
     private static final float BRIGHTNESS_DEFAULT = 0.5f;
     private static final String ETC_DIR = "etc";
     private static final String DISPLAY_CONFIG_DIR = "displayconfig";
@@ -513,7 +515,6 @@
     private static final int DEFAULT_PEAK_REFRESH_RATE = 0;
     private static final int DEFAULT_REFRESH_RATE = 60;
     private static final int DEFAULT_REFRESH_RATE_IN_HBM = 0;
-    private static final int DEFAULT_LOW_REFRESH_RATE = 60;
     private static final int DEFAULT_HIGH_REFRESH_RATE = 0;
     private static final float[] DEFAULT_BRIGHTNESS_THRESHOLDS = new float[]{};
 
diff --git a/services/core/java/com/android/server/display/DisplayDeviceInfo.java b/services/core/java/com/android/server/display/DisplayDeviceInfo.java
index 213ee64..3529b04 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceInfo.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceInfo.java
@@ -16,6 +16,8 @@
 
 package com.android.server.display;
 
+import static android.view.Display.Mode.INVALID_MODE_ID;
+
 import android.hardware.display.DeviceProductInfo;
 import android.hardware.display.DisplayViewport;
 import android.util.DisplayMetrics;
@@ -275,6 +277,11 @@
     public int defaultModeId;
 
     /**
+     * The mode of the display which is preferred by user.
+     */
+    public int userPreferredModeId = INVALID_MODE_ID;
+
+    /**
      * The supported modes of the display.
      */
     public Display.Mode[] supportedModes = Display.Mode.EMPTY_ARRAY;
@@ -472,6 +479,7 @@
                 || modeId != other.modeId
                 || renderFrameRate != other.renderFrameRate
                 || defaultModeId != other.defaultModeId
+                || userPreferredModeId != other.userPreferredModeId
                 || !Arrays.equals(supportedModes, other.supportedModes)
                 || !Arrays.equals(supportedColorModes, other.supportedColorModes)
                 || !Objects.equals(hdrCapabilities, other.hdrCapabilities)
@@ -517,6 +525,7 @@
         modeId = other.modeId;
         renderFrameRate = other.renderFrameRate;
         defaultModeId = other.defaultModeId;
+        userPreferredModeId = other.userPreferredModeId;
         supportedModes = other.supportedModes;
         colorMode = other.colorMode;
         supportedColorModes = other.supportedColorModes;
@@ -559,6 +568,7 @@
         sb.append(", modeId ").append(modeId);
         sb.append(", renderFrameRate ").append(renderFrameRate);
         sb.append(", defaultModeId ").append(defaultModeId);
+        sb.append(", userPreferredModeId ").append(userPreferredModeId);
         sb.append(", supportedModes ").append(Arrays.toString(supportedModes));
         sb.append(", colorMode ").append(colorMode);
         sb.append(", supportedColorModes ").append(Arrays.toString(supportedColorModes));
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 46ef6c3..e942c17 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -557,7 +557,7 @@
         mLogicalDisplayMapper = new LogicalDisplayMapper(mContext,
                 new FoldSettingProvider(mContext, new SettingsWrapper()), mDisplayDeviceRepo,
                 new LogicalDisplayListener(), mSyncRoot, mHandler, mFlags);
-        mDisplayModeDirector = new DisplayModeDirector(context, mHandler);
+        mDisplayModeDirector = new DisplayModeDirector(context, mHandler, mFlags);
         mBrightnessSynchronizer = new BrightnessSynchronizer(mContext);
         Resources resources = mContext.getResources();
         mDefaultDisplayDefaultColorMode = mContext.getResources().getInteger(
@@ -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
@@ -2028,9 +2028,6 @@
         mDisplayBrightnesses.delete(displayId);
         DisplayManagerGlobal.invalidateLocalDisplayInfoCaches();
 
-        sendDisplayEventLocked(display, event);
-        scheduleTraversalLocked(false);
-
         if (mDisplayWindowPolicyControllers.contains(displayId)) {
             final IVirtualDevice virtualDevice =
                     mDisplayWindowPolicyControllers.removeReturnOld(displayId).first;
@@ -2041,6 +2038,9 @@
                 });
             }
         }
+
+        sendDisplayEventLocked(display, event);
+        scheduleTraversalLocked(false);
     }
 
     private void handleLogicalDisplaySwappedLocked(@NonNull LogicalDisplay display) {
@@ -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 924b1b3..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;
@@ -640,6 +648,7 @@
                 mInfo.modeId = mActiveModeId;
                 mInfo.renderFrameRate = mActiveRenderFrameRate;
                 mInfo.defaultModeId = getPreferredModeId();
+                mInfo.userPreferredModeId = mUserPreferredModeId;
                 mInfo.supportedModes = getDisplayModes(mSupportedModes);
                 mInfo.colorMode = mActiveColorMode;
                 mInfo.allmSupported = mAllmSupported;
@@ -745,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;
@@ -812,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
@@ -835,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);
+                                }
                             }
                         }
                     }
@@ -857,6 +907,7 @@
                         }
                     }
 
+
                     private void setDisplayBrightness(float brightnessState,
                             float sdrBrightnessState) {
                         // brightnessState includes invalid, off and full range.
@@ -1344,6 +1395,7 @@
 
     public interface DisplayEventListener {
         void onHotplug(long timestampNanos, long physicalDisplayId, boolean connected);
+        void onHotplugConnectionError(long timestampNanos, int connectionError);
         void onModeChanged(long timestampNanos, long physicalDisplayId, int modeId,
                 long renderPeriod);
         void onFrameRateOverridesChanged(long timestampNanos, long physicalDisplayId,
@@ -1366,6 +1418,11 @@
         }
 
         @Override
+        public void onHotplugConnectionError(long timestampNanos, int errorCode) {
+            mListener.onHotplugConnectionError(timestampNanos, errorCode);
+        }
+
+        @Override
         public void onModeChanged(long timestampNanos, long physicalDisplayId, int modeId,
                 long renderPeriod) {
             mListener.onModeChanged(timestampNanos, physicalDisplayId, modeId, renderPeriod);
@@ -1391,6 +1448,15 @@
         }
 
         @Override
+        public void onHotplugConnectionError(long timestampNanos, int connectionError) {
+            if (DEBUG) {
+                Slog.d(TAG, "onHotplugConnectionError("
+                        + "timestampNanos=" + timestampNanos
+                        + ", connectionError=" + connectionError + ")");
+            }
+        }
+
+        @Override
         public void onModeChanged(long timestampNanos, long physicalDisplayId, int modeId,
                 long renderPeriod) {
             if (DEBUG) {
diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java
index 0405ebe..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.
      */
@@ -470,6 +473,7 @@
             mBaseDisplayInfo.modeId = deviceInfo.modeId;
             mBaseDisplayInfo.renderFrameRate = deviceInfo.renderFrameRate;
             mBaseDisplayInfo.defaultModeId = deviceInfo.defaultModeId;
+            mBaseDisplayInfo.userPreferredModeId = deviceInfo.userPreferredModeId;
             mBaseDisplayInfo.supportedModes = Arrays.copyOf(
                     deviceInfo.supportedModes, deviceInfo.supportedModes.length);
             mBaseDisplayInfo.colorMode = deviceInfo.colorMode;
@@ -940,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 3f6bf1a..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,26 @@
             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);
+
+    private final FlagState mUserPreferredModeVoteState = new FlagState(
+            Flags.FLAG_ENABLE_USER_PREFERRED_MODE_VOTE,
+            Flags::enableUserPreferredModeVote);
+
+    private final FlagState mExternalDisplayLimitModeState = new FlagState(
+            Flags.FLAG_ENABLE_MODE_LIMIT_FOR_EXTERNAL_DISPLAY,
+            Flags::enableModeLimitForExternalDisplay);
+
+    private final FlagState mDisplaysRefreshRatesSynchronizationState = new FlagState(
+            Flags.FLAG_ENABLE_DISPLAYS_REFRESH_RATES_SYNCHRONIZATION,
+            Flags::enableDisplaysRefreshRatesSynchronization);
+
     /** Returns whether connected display management is enabled or not. */
     public boolean isConnectedDisplayManagementEnabled() {
         return mConnectedDisplayManagementFlagState.isEnabled();
@@ -68,6 +88,38 @@
         return mAdaptiveToneImprovements1.isEnabled();
     }
 
+    /** Returns whether resolution range voting feature is enabled or not. */
+    public boolean isDisplayResolutionRangeVotingEnabled() {
+        return mDisplayResolutionRangeVotingState.isEnabled();
+    }
+
+    /**
+     * @return Whether user preferred mode is added as a vote in
+     *      {@link com.android.server.display.mode.DisplayModeDirector}
+     */
+    public boolean isUserPreferredModeVoteEnabled() {
+        return mUserPreferredModeVoteState.isEnabled();
+    }
+
+    /**
+     * @return Whether external display mode limitation is enabled.
+     */
+    public boolean isExternalDisplayLimitModeEnabled() {
+        return mExternalDisplayLimitModeState.isEnabled();
+    }
+
+    /**
+     * @return Whether displays refresh rate synchronization is enabled.
+     */
+    public boolean isDisplaysRefreshRatesSynchronizationEnabled() {
+        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 4d86004..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
@@ -5,7 +5,7 @@
 flag {
     name: "enable_connected_display_management"
     namespace: "display_manager"
-    description: "Feature flag for Connected Display managment"
+    description: "Feature flag for Connected Display management"
     bug: "280739508"
     is_fixed_read_only: true
 }
@@ -34,3 +34,41 @@
     is_fixed_read_only: true
 }
 
+flag {
+    name: "enable_display_resolution_range_voting"
+    namespace: "display_manager"
+    description: "Feature flag to enable voting for ranges of resolutions"
+    bug: "299297058"
+    is_fixed_read_only: true
+}
+
+flag {
+    name: "enable_user_preferred_mode_vote"
+    namespace: "display_manager"
+    description: "Feature flag to use voting for UserPreferredMode for display"
+    bug: "297018612"
+    is_fixed_read_only: true
+}
+
+flag {
+    name: "enable_mode_limit_for_external_display"
+    namespace: "display_manager"
+    description: "Feature limiting external display resolution and refresh rate"
+    bug: "242093547"
+    is_fixed_read_only: true
+}
+
+flag {
+    name: "enable_displays_refresh_rates_synchronization"
+    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/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
index 2c2af3f..71ea8cc 100644
--- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
@@ -19,6 +19,9 @@
 import static android.hardware.display.DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED;
 import static android.hardware.display.DisplayManagerInternal.REFRESH_RATE_LIMIT_HIGH_BRIGHTNESS_MODE;
 import static android.os.PowerManager.BRIGHTNESS_INVALID_FLOAT;
+import static android.view.Display.Mode.INVALID_MODE_ID;
+
+import static com.android.server.display.DisplayDeviceConfig.DEFAULT_LOW_REFRESH_RATE;
 
 import android.annotation.IntegerRes;
 import android.annotation.NonNull;
@@ -71,6 +74,7 @@
 import com.android.server.LocalServices;
 import com.android.server.display.DisplayDeviceConfig;
 import com.android.server.display.feature.DeviceConfigParameterProvider;
+import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.utils.AmbientFilter;
 import com.android.server.display.utils.AmbientFilterFactory;
 import com.android.server.display.utils.DeviceConfigParsingUtils;
@@ -84,9 +88,11 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Date;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.function.Function;
 import java.util.function.IntSupplier;
@@ -96,6 +102,8 @@
  * picked by the system based on system-wide and display-specific configuration.
  */
 public class DisplayModeDirector {
+    public static final float SYNCHRONIZED_REFRESH_RATE_TARGET = DEFAULT_LOW_REFRESH_RATE;
+    public static final float SYNCHRONIZED_REFRESH_RATE_TOLERANCE = 1;
     private static final String TAG = "DisplayModeDirector";
     private boolean mLoggingEnabled;
 
@@ -151,12 +159,38 @@
     @DisplayManager.SwitchingType
     private int mModeSwitchingType = DisplayManager.SWITCHING_TYPE_WITHIN_GROUPS;
 
-    public DisplayModeDirector(@NonNull Context context, @NonNull Handler handler) {
-        this(context, handler, new RealInjector(context));
+    /**
+     * Whether resolution range voting feature is enabled.
+     */
+    private final boolean mIsDisplayResolutionRangeVotingEnabled;
+
+    /**
+     * Whether user preferred mode voting feature is enabled.
+     */
+    private final boolean mIsUserPreferredModeVoteEnabled;
+
+    /**
+     * Whether limit display mode feature is enabled.
+     */
+    private final boolean mIsExternalDisplayLimitModeEnabled;
+
+    private final boolean mIsDisplaysRefreshRatesSynchronizationEnabled;
+
+    public DisplayModeDirector(@NonNull Context context, @NonNull Handler handler,
+            @NonNull DisplayManagerFlags displayManagerFlags) {
+        this(context, handler, new RealInjector(context), displayManagerFlags);
     }
 
     public DisplayModeDirector(@NonNull Context context, @NonNull Handler handler,
-            @NonNull Injector injector) {
+            @NonNull Injector injector,
+            @NonNull DisplayManagerFlags displayManagerFlags) {
+        mIsDisplayResolutionRangeVotingEnabled = displayManagerFlags
+                .isDisplayResolutionRangeVotingEnabled();
+        mIsUserPreferredModeVoteEnabled = displayManagerFlags.isUserPreferredModeVoteEnabled();
+        mIsExternalDisplayLimitModeEnabled = displayManagerFlags
+            .isExternalDisplayLimitModeEnabled();
+        mIsDisplaysRefreshRatesSynchronizationEnabled = displayManagerFlags
+            .isDisplaysRefreshRatesSynchronizationEnabled();
         mContext = context;
         mHandler = new DisplayModeDirectorHandler(handler.getLooper());
         mInjector = injector;
@@ -230,6 +264,8 @@
         public float maxRenderFrameRate;
         public int width;
         public int height;
+        public int minWidth;
+        public int minHeight;
         public boolean disableRefreshRateSwitching;
         public float appRequestBaseModeRefreshRate;
 
@@ -244,6 +280,8 @@
             maxRenderFrameRate = Float.POSITIVE_INFINITY;
             width = Vote.INVALID_SIZE;
             height = Vote.INVALID_SIZE;
+            minWidth = 0;
+            minHeight = 0;
             disableRefreshRateSwitching = false;
             appRequestBaseModeRefreshRate = 0f;
         }
@@ -256,6 +294,8 @@
                     + ", maxRenderFrameRate=" + maxRenderFrameRate
                     + ", width=" + width
                     + ", height=" + height
+                    + ", minWidth=" + minWidth
+                    + ", minHeight=" + minHeight
                     + ", disableRefreshRateSwitching=" + disableRefreshRateSwitching
                     + ", appRequestBaseModeRefreshRate=" + appRequestBaseModeRefreshRate;
         }
@@ -277,7 +317,6 @@
                 continue;
             }
 
-
             // For physical refresh rates, just use the tightest bounds of all the votes.
             // The refresh rate cannot be lower than the minimal render frame rate.
             final float minPhysicalRefreshRate = Math.max(vote.refreshRateRanges.physical.min,
@@ -298,10 +337,18 @@
             // For display size, disable refresh rate switching and base mode refresh rate use only
             // the first vote we come across (i.e. the highest priority vote that includes the
             // attribute).
-            if (summary.height == Vote.INVALID_SIZE && summary.width == Vote.INVALID_SIZE
-                    && vote.height > 0 && vote.width > 0) {
-                summary.width = vote.width;
-                summary.height = vote.height;
+            if (vote.height > 0 && vote.width > 0) {
+                if (summary.width == Vote.INVALID_SIZE && summary.height == Vote.INVALID_SIZE) {
+                    summary.width = vote.width;
+                    summary.height = vote.height;
+                    summary.minWidth = vote.minWidth;
+                    summary.minHeight = vote.minHeight;
+                } else if (mIsDisplayResolutionRangeVotingEnabled) {
+                    summary.width = Math.min(summary.width, vote.width);
+                    summary.height = Math.min(summary.height, vote.height);
+                    summary.minWidth = Math.max(summary.minWidth, vote.minWidth);
+                    summary.minHeight = Math.max(summary.minHeight, vote.minHeight);
+                }
             }
             if (!summary.disableRefreshRateSwitching && vote.disableRefreshRateSwitching) {
                 summary.disableRefreshRateSwitching = true;
@@ -413,6 +460,8 @@
                         || primarySummary.width == Vote.INVALID_SIZE) {
                     primarySummary.width = defaultMode.getPhysicalWidth();
                     primarySummary.height = defaultMode.getPhysicalHeight();
+                } else if (mIsDisplayResolutionRangeVotingEnabled) {
+                    updateSummaryWithBestAllowedResolution(modes, primarySummary);
                 }
 
                 availableModes = filterModes(modes, primarySummary);
@@ -654,6 +703,38 @@
         return availableModes;
     }
 
+    private void updateSummaryWithBestAllowedResolution(final Display.Mode[] supportedModes,
+            VoteSummary outSummary) {
+        final int maxAllowedWidth = outSummary.width;
+        final int maxAllowedHeight = outSummary.height;
+        if (mLoggingEnabled) {
+            Slog.i(TAG, "updateSummaryWithBestAllowedResolution " + outSummary);
+        }
+        outSummary.width = Vote.INVALID_SIZE;
+        outSummary.height = Vote.INVALID_SIZE;
+
+        int maxNumberOfPixels = 0;
+        for (Display.Mode mode : supportedModes) {
+            if (mode.getPhysicalWidth() > maxAllowedWidth
+                    || mode.getPhysicalHeight() > maxAllowedHeight
+                    || mode.getPhysicalWidth() < outSummary.minWidth
+                    || mode.getPhysicalHeight() < outSummary.minHeight) {
+                continue;
+            }
+
+            int numberOfPixels = mode.getPhysicalHeight() * mode.getPhysicalWidth();
+            if (numberOfPixels > maxNumberOfPixels || (mode.getPhysicalWidth() == maxAllowedWidth
+                    && mode.getPhysicalHeight() == maxAllowedHeight)) {
+                if (mLoggingEnabled) {
+                    Slog.i(TAG, "updateSummaryWithBestAllowedResolution updated with " + mode);
+                }
+                maxNumberOfPixels = numberOfPixels;
+                outSummary.width = mode.getPhysicalWidth();
+                outSummary.height = mode.getPhysicalHeight();
+            }
+        }
+    }
+
     /**
      * Gets the observer responsible for application display mode requests.
      */
@@ -1393,11 +1474,38 @@
         private final Context mContext;
         private final Handler mHandler;
         private final VotesStorage mVotesStorage;
+        private int mExternalDisplayPeakWidth;
+        private int mExternalDisplayPeakHeight;
+        private int mExternalDisplayPeakRefreshRate;
+        private final boolean mRefreshRateSynchronizationEnabled;
+        private final Set<Integer> mExternalDisplaysConnected = new HashSet<>();
 
         DisplayObserver(Context context, Handler handler, VotesStorage votesStorage) {
             mContext = context;
             mHandler = handler;
             mVotesStorage = votesStorage;
+            mExternalDisplayPeakRefreshRate = mContext.getResources().getInteger(
+                        R.integer.config_externalDisplayPeakRefreshRate);
+            mExternalDisplayPeakWidth = mContext.getResources().getInteger(
+                        R.integer.config_externalDisplayPeakWidth);
+            mExternalDisplayPeakHeight = mContext.getResources().getInteger(
+                        R.integer.config_externalDisplayPeakHeight);
+            mRefreshRateSynchronizationEnabled = mContext.getResources().getBoolean(
+                        R.bool.config_refreshRateSynchronizationEnabled);
+        }
+
+        private boolean isExternalDisplayLimitModeEnabled() {
+            return mExternalDisplayPeakWidth > 0
+                && mExternalDisplayPeakHeight > 0
+                && mExternalDisplayPeakRefreshRate > 0
+                && mIsExternalDisplayLimitModeEnabled
+                && mIsDisplayResolutionRangeVotingEnabled
+                && mIsUserPreferredModeVoteEnabled;
+        }
+
+        private boolean isRefreshRateSynchronizationEnabled() {
+            return mRefreshRateSynchronizationEnabled
+                && mIsDisplaysRefreshRatesSynchronizationEnabled;
         }
 
         public void observe() {
@@ -1428,6 +1536,9 @@
             DisplayInfo displayInfo = getDisplayInfo(displayId);
             updateDisplayModes(displayId, displayInfo);
             updateLayoutLimitedFrameRate(displayId, displayInfo);
+            updateUserSettingDisplayPreferredSize(displayInfo);
+            updateDisplaysPeakRefreshRateAndResolution(displayInfo);
+            addDisplaysSynchronizedPeakRefreshRate(displayInfo);
         }
 
         @Override
@@ -1437,6 +1548,9 @@
                 mDefaultModeByDisplay.remove(displayId);
             }
             updateLayoutLimitedFrameRate(displayId, null);
+            removeUserSettingDisplayPreferredSize(displayId);
+            removeDisplaysPeakRefreshRateAndResolution(displayId);
+            removeDisplaysSynchronizedPeakRefreshRate(displayId);
         }
 
         @Override
@@ -1444,6 +1558,7 @@
             DisplayInfo displayInfo = getDisplayInfo(displayId);
             updateDisplayModes(displayId, displayInfo);
             updateLayoutLimitedFrameRate(displayId, displayInfo);
+            updateUserSettingDisplayPreferredSize(displayInfo);
         }
 
         @Nullable
@@ -1460,6 +1575,111 @@
             mVotesStorage.updateVote(displayId, Vote.PRIORITY_LAYOUT_LIMITED_FRAME_RATE, vote);
         }
 
+        private void removeUserSettingDisplayPreferredSize(int displayId) {
+            if (!mIsUserPreferredModeVoteEnabled) {
+                return;
+            }
+            mVotesStorage.updateVote(displayId, Vote.PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE,
+                    null);
+        }
+
+        private void updateUserSettingDisplayPreferredSize(@Nullable DisplayInfo info) {
+            if (info == null || !mIsUserPreferredModeVoteEnabled) {
+                return;
+            }
+
+            var preferredMode = findDisplayPreferredMode(info);
+            if (preferredMode == null) {
+                removeUserSettingDisplayPreferredSize(info.displayId);
+                return;
+            }
+
+            mVotesStorage.updateVote(info.displayId,
+                    Vote.PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE,
+                    Vote.forSize(/* width */ preferredMode.getPhysicalWidth(),
+                            /* height */ preferredMode.getPhysicalHeight()));
+        }
+
+        @Nullable
+        private Display.Mode findDisplayPreferredMode(@NonNull DisplayInfo info) {
+            if (info.userPreferredModeId == INVALID_MODE_ID) {
+                return null;
+            }
+            for (var mode : info.supportedModes) {
+                if (mode.getModeId() == info.userPreferredModeId) {
+                    return mode;
+                }
+            }
+            return null;
+        }
+
+        private void removeDisplaysPeakRefreshRateAndResolution(int displayId) {
+            if (!isExternalDisplayLimitModeEnabled()) {
+                return;
+            }
+
+            mVotesStorage.updateVote(displayId,
+                    Vote.PRIORITY_LIMIT_MODE, null);
+        }
+
+        private void updateDisplaysPeakRefreshRateAndResolution(@Nullable final DisplayInfo info) {
+            // Only consider external display, only in case the refresh rate and resolution limits
+            // are non-zero.
+            if (info == null || info.type != Display.TYPE_EXTERNAL
+                    || !isExternalDisplayLimitModeEnabled()) {
+                return;
+            }
+
+            mVotesStorage.updateVote(info.displayId,
+                    Vote.PRIORITY_LIMIT_MODE,
+                    Vote.forSizeAndPhysicalRefreshRatesRange(
+                            /* minWidth */ 0, /* minHeight */ 0,
+                            mExternalDisplayPeakWidth,
+                            mExternalDisplayPeakHeight,
+                            /* minPhysicalRefreshRate */ 0,
+                            mExternalDisplayPeakRefreshRate));
+        }
+
+        /**
+         * Sets 60Hz target refresh rate as the vote with
+         * {@link Vote#PRIORITY_SYNCHRONIZED_REFRESH_RATE} priority.
+         */
+        private void addDisplaysSynchronizedPeakRefreshRate(@Nullable final DisplayInfo info) {
+            if (info == null || info.type != Display.TYPE_EXTERNAL
+                    || !isRefreshRateSynchronizationEnabled()) {
+                return;
+            }
+            synchronized (mLock) {
+                mExternalDisplaysConnected.add(info.displayId);
+                if (mExternalDisplaysConnected.size() != 1) {
+                    return;
+                }
+            }
+            // set minRefreshRate as the max refresh rate.
+            mVotesStorage.updateGlobalVote(Vote.PRIORITY_SYNCHRONIZED_REFRESH_RATE,
+                    Vote.forPhysicalRefreshRates(
+                            SYNCHRONIZED_REFRESH_RATE_TARGET
+                                - SYNCHRONIZED_REFRESH_RATE_TOLERANCE,
+                            SYNCHRONIZED_REFRESH_RATE_TARGET
+                                + SYNCHRONIZED_REFRESH_RATE_TOLERANCE));
+        }
+
+        private void removeDisplaysSynchronizedPeakRefreshRate(final int displayId) {
+            if (!isRefreshRateSynchronizationEnabled()) {
+                return;
+            }
+            synchronized (mLock) {
+                if (!mExternalDisplaysConnected.contains(displayId)) {
+                    return;
+                }
+                mExternalDisplaysConnected.remove(displayId);
+                if (mExternalDisplaysConnected.size() != 0) {
+                    return;
+                }
+            }
+            mVotesStorage.updateGlobalVote(Vote.PRIORITY_SYNCHRONIZED_REFRESH_RATE, null);
+        }
+
         private void updateDisplayModes(int displayId, @Nullable DisplayInfo info) {
             if (info == null) {
                 return;
diff --git a/services/core/java/com/android/server/display/mode/Vote.java b/services/core/java/com/android/server/display/mode/Vote.java
index a42d8f2..b6a6069 100644
--- a/services/core/java/com/android/server/display/mode/Vote.java
+++ b/services/core/java/com/android/server/display/mode/Vote.java
@@ -18,6 +18,8 @@
 
 import android.view.SurfaceControl;
 
+import java.util.Objects;
+
 final class Vote {
     // DEFAULT_RENDER_FRAME_RATE votes for render frame rate [0, DEFAULT]. As the lowest
     // priority vote, it's overridden by all other considerations. It acts to set a default
@@ -36,12 +38,15 @@
     // It votes [minRefreshRate, Float.POSITIVE_INFINITY]
     static final int PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE = 3;
 
+    // User setting preferred display resolution.
+    static final int PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE = 4;
+
     // APP_REQUEST_RENDER_FRAME_RATE_RANGE is used to for internal apps to limit the render
     // frame rate in certain cases, mostly to preserve power.
     // @see android.view.WindowManager.LayoutParams#preferredMinRefreshRate
     // @see android.view.WindowManager.LayoutParams#preferredMaxRefreshRate
     // It votes to [preferredMinRefreshRate, preferredMaxRefreshRate].
-    static final int PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE = 4;
+    static final int PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE = 5;
 
     // We split the app request into different priorities in case we can satisfy one desire
     // without the other.
@@ -67,40 +72,47 @@
     // The preferred refresh rate is set on the main surface of the app outside of
     // DisplayModeDirector.
     // @see com.android.server.wm.WindowState#updateFrameRateSelectionPriorityIfNeeded
-    static final int PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE = 5;
-    static final int PRIORITY_APP_REQUEST_SIZE = 6;
+    static final int PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE = 6;
+
+    static final int PRIORITY_APP_REQUEST_SIZE = 7;
 
     // SETTING_PEAK_RENDER_FRAME_RATE has a high priority and will restrict the bounds of the
     // rest of low priority voters. It votes [0, max(PEAK, MIN)]
-    static final int PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE = 7;
+    static final int PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE = 8;
+
+    // Restrict all displays to 60Hz when external display is connected. It votes [59Hz, 61Hz].
+    static final int PRIORITY_SYNCHRONIZED_REFRESH_RATE = 9;
+
+    // Restrict displays max available resolution and refresh rates. It votes [0, LIMIT]
+    static final int PRIORITY_LIMIT_MODE = 10;
 
     // To avoid delay in switching between 60HZ -> 90HZ when activating LHBM, set refresh
     // rate to max value (same as for PRIORITY_UDFPS) on lock screen
-    static final int PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE = 8;
+    static final int PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE = 11;
 
     // For concurrent displays we want to limit refresh rate on all displays
-    static final int PRIORITY_LAYOUT_LIMITED_FRAME_RATE = 9;
+    static final int PRIORITY_LAYOUT_LIMITED_FRAME_RATE = 12;
 
     // LOW_POWER_MODE force the render frame rate to [0, 60HZ] if
     // Settings.Global.LOW_POWER_MODE is on.
-    static final int PRIORITY_LOW_POWER_MODE = 10;
+    static final int PRIORITY_LOW_POWER_MODE = 13;
 
     // PRIORITY_FLICKER_REFRESH_RATE_SWITCH votes for disabling refresh rate switching. If the
     // higher priority voters' result is a range, it will fix the rate to a single choice.
     // It's used to avoid refresh rate switches in certain conditions which may result in the
     // user seeing the display flickering when the switches occur.
-    static final int PRIORITY_FLICKER_REFRESH_RATE_SWITCH = 11;
+    static final int PRIORITY_FLICKER_REFRESH_RATE_SWITCH = 14;
 
     // Force display to [0, 60HZ] if skin temperature is at or above CRITICAL.
-    static final int PRIORITY_SKIN_TEMPERATURE = 12;
+    static final int PRIORITY_SKIN_TEMPERATURE = 15;
 
     // The proximity sensor needs the refresh rate to be locked in order to function, so this is
     // set to a high priority.
-    static final int PRIORITY_PROXIMITY = 13;
+    static final int PRIORITY_PROXIMITY = 16;
 
     // The Under-Display Fingerprint Sensor (UDFPS) needs the refresh rate to be locked in order
     // to function, so this needs to be the highest priority of all votes.
-    static final int PRIORITY_UDFPS = 14;
+    static final int PRIORITY_UDFPS = 17;
 
     // Whenever a new priority is added, remember to update MIN_PRIORITY, MAX_PRIORITY, and
     // APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF, as well as priorityToString.
@@ -127,6 +139,14 @@
      */
     public final int height;
     /**
+     * Min requested width of the display in pixels, or 0;
+     */
+    public final int minWidth;
+    /**
+     * Min requested height of the display in pixels, or 0;
+     */
+    public final int minHeight;
+    /**
      * Information about the refresh rate frame rate ranges DM would like to set the display to.
      */
     public final SurfaceControl.RefreshRateRanges refreshRateRanges;
@@ -144,42 +164,82 @@
     public final float appRequestBaseModeRefreshRate;
 
     static Vote forPhysicalRefreshRates(float minRefreshRate, float maxRefreshRate) {
-        return new Vote(INVALID_SIZE, INVALID_SIZE, minRefreshRate, maxRefreshRate, 0,
-                Float.POSITIVE_INFINITY,
-                minRefreshRate == maxRefreshRate, 0f);
+        return new Vote(/* minWidth= */ 0, /* minHeight= */ 0,
+                /* width= */ INVALID_SIZE, /* height= */ INVALID_SIZE,
+                /* minPhysicalRefreshRate= */ minRefreshRate,
+                /* maxPhysicalRefreshRate= */ maxRefreshRate,
+                /* minRenderFrameRate= */ 0,
+                /* maxRenderFrameRate= */ Float.POSITIVE_INFINITY,
+                /* disableRefreshRateSwitching= */ minRefreshRate == maxRefreshRate,
+                /* baseModeRefreshRate= */ 0f);
     }
 
     static Vote forRenderFrameRates(float minFrameRate, float maxFrameRate) {
-        return new Vote(INVALID_SIZE, INVALID_SIZE, 0, Float.POSITIVE_INFINITY, minFrameRate,
+        return new Vote(/* minWidth= */ 0, /* minHeight= */ 0,
+                /* width= */ INVALID_SIZE, /* height= */ INVALID_SIZE,
+                /* minPhysicalRefreshRate= */ 0,
+                /* maxPhysicalRefreshRate= */ Float.POSITIVE_INFINITY,
+                minFrameRate,
                 maxFrameRate,
-                false, 0f);
+                /* disableRefreshRateSwitching= */ false,
+                /* baseModeRefreshRate= */ 0f);
     }
 
     static Vote forSize(int width, int height) {
-        return new Vote(width, height, 0, Float.POSITIVE_INFINITY, 0, Float.POSITIVE_INFINITY,
-                false,
-                0f);
+        return new Vote(/* minWidth= */ width, /* minHeight= */ height,
+                width, height,
+                /* minPhysicalRefreshRate= */ 0,
+                /* maxPhysicalRefreshRate= */ Float.POSITIVE_INFINITY,
+                /* minRenderFrameRate= */ 0,
+                /* maxRenderFrameRate= */ Float.POSITIVE_INFINITY,
+                /* disableRefreshRateSwitching= */ false,
+                /* baseModeRefreshRate= */ 0f);
+    }
+
+    static Vote forSizeAndPhysicalRefreshRatesRange(int minWidth, int minHeight,
+            int width, int height, float minRefreshRate, float maxRefreshRate) {
+        return new Vote(minWidth, minHeight,
+                width, height,
+                minRefreshRate,
+                maxRefreshRate,
+                /* minRenderFrameRate= */ 0,
+                /* maxRenderFrameRate= */ Float.POSITIVE_INFINITY,
+                /* disableRefreshRateSwitching= */ minRefreshRate == maxRefreshRate,
+                /* baseModeRefreshRate= */ 0f);
     }
 
     static Vote forDisableRefreshRateSwitching() {
-        return new Vote(INVALID_SIZE, INVALID_SIZE, 0, Float.POSITIVE_INFINITY, 0,
-                Float.POSITIVE_INFINITY, true,
-                0f);
+        return new Vote(/* minWidth= */ 0, /* minHeight= */ 0,
+                /* width= */ INVALID_SIZE, /* height= */ INVALID_SIZE,
+                /* minPhysicalRefreshRate= */ 0,
+                /* maxPhysicalRefreshRate= */ Float.POSITIVE_INFINITY,
+                /* minRenderFrameRate= */ 0,
+                /* maxRenderFrameRate= */ Float.POSITIVE_INFINITY,
+                /* disableRefreshRateSwitching= */ true,
+                /* baseModeRefreshRate= */ 0f);
     }
 
     static Vote forBaseModeRefreshRate(float baseModeRefreshRate) {
-        return new Vote(INVALID_SIZE, INVALID_SIZE, 0, Float.POSITIVE_INFINITY, 0,
-                Float.POSITIVE_INFINITY, false,
-                baseModeRefreshRate);
+        return new Vote(/* minWidth= */ 0, /* minHeight= */ 0,
+                /* width= */ INVALID_SIZE, /* height= */ INVALID_SIZE,
+                /* minPhysicalRefreshRate= */ 0,
+                /* maxPhysicalRefreshRate= */ Float.POSITIVE_INFINITY,
+                /* minRenderFrameRate= */ 0,
+                /* maxRenderFrameRate= */ Float.POSITIVE_INFINITY,
+                /* disableRefreshRateSwitching= */ false,
+                /* baseModeRefreshRate= */ baseModeRefreshRate);
     }
 
-    private Vote(int width, int height,
+    private Vote(int minWidth, int minHeight,
+            int width, int height,
             float minPhysicalRefreshRate,
             float maxPhysicalRefreshRate,
             float minRenderFrameRate,
             float maxRenderFrameRate,
             boolean disableRefreshRateSwitching,
             float baseModeRefreshRate) {
+        this.minWidth = minWidth;
+        this.minHeight = minHeight;
         this.width = width;
         this.height = height;
         this.refreshRateRanges = new SurfaceControl.RefreshRateRanges(
@@ -215,6 +275,12 @@
                 return "PRIORITY_UDFPS";
             case PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE:
                 return "PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE";
+            case PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE:
+                return "PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE";
+            case PRIORITY_LIMIT_MODE:
+                return "PRIORITY_LIMIT_MODE";
+            case PRIORITY_SYNCHRONIZED_REFRESH_RATE:
+                return "PRIORITY_SYNCHRONIZED_REFRESH_RATE";
             case PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE:
                 return "PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE";
             case PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE:
@@ -229,9 +295,29 @@
     @Override
     public String toString() {
         return "Vote: {"
-                + "width: " + width + ", height: " + height
+                + "minWidth: " + minWidth + ", minHeight: " + minHeight
+                + ", width: " + width + ", height: " + height
                 + ", refreshRateRanges: " + refreshRateRanges
                 + ", disableRefreshRateSwitching: " + disableRefreshRateSwitching
                 + ", appRequestBaseModeRefreshRate: "  + appRequestBaseModeRefreshRate + "}";
     }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(minWidth, minHeight, width, height, refreshRateRanges,
+                disableRefreshRateSwitching, appRequestBaseModeRefreshRate);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof Vote)) return false;
+        final var vote = (Vote) o;
+        return  minWidth == vote.minWidth && minHeight == vote.minHeight
+                && width == vote.width && height == vote.height
+                && disableRefreshRateSwitching == vote.disableRefreshRateSwitching
+                && Float.compare(vote.appRequestBaseModeRefreshRate,
+                        appRequestBaseModeRefreshRate) == 0
+                && refreshRateRanges.equals(vote.refreshRateRanges);
+    }
 }
diff --git a/services/core/java/com/android/server/display/mode/VotesStorage.java b/services/core/java/com/android/server/display/mode/VotesStorage.java
index bdd2ab7..49c587a 100644
--- a/services/core/java/com/android/server/display/mode/VotesStorage.java
+++ b/services/core/java/com/android/server/display/mode/VotesStorage.java
@@ -31,7 +31,8 @@
     private static final String TAG = "VotesStorage";
     // Special ID used to indicate that given vote is to be applied globally, rather than to a
     // specific display.
-    private static final int GLOBAL_ID = -1;
+    @VisibleForTesting
+    static final int GLOBAL_ID = -1;
 
     private boolean mLoggingEnabled;
 
@@ -91,6 +92,7 @@
                     + ", vote=" + vote);
             return;
         }
+        boolean changed = false;
         SparseArray<Vote> votes;
         synchronized (mStorageLock) {
             if (mVotesByDisplay.contains(displayId)) {
@@ -99,10 +101,13 @@
                 votes = new SparseArray<>();
                 mVotesByDisplay.put(displayId, votes);
             }
-            if (vote != null) {
+            var currentVote = votes.get(priority);
+            if (vote != null && !vote.equals(currentVote)) {
                 votes.put(priority, vote);
-            } else {
+                changed = true;
+            } else if (vote == null && currentVote != null) {
                 votes.remove(priority);
+                changed = true;
             }
         }
         Trace.traceCounter(Trace.TRACE_TAG_POWER,
@@ -111,7 +116,9 @@
         if (mLoggingEnabled) {
             Slog.i(TAG, "Updated votes for display=" + displayId + " votes=" + votes);
         }
-        mListener.onChanged();
+        if (changed) {
+            mListener.onChanged();
+        }
     }
 
     /** dump class values, for debugging */
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/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index d3ad6c4..3435e56 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -92,7 +92,6 @@
 import android.os.LocaleList;
 import android.os.Looper;
 import android.os.Message;
-import android.os.Parcel;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
@@ -1868,21 +1867,6 @@
                         false /* enabledOnly */));
     }
 
-    @Override
-    public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
-            throws RemoteException {
-        try {
-            return super.onTransact(code, data, reply, flags);
-        } catch (RuntimeException e) {
-            // The input method manager only throws security exceptions, so let's
-            // log all others.
-            if (!(e instanceof SecurityException)) {
-                Slog.wtf(TAG, "Input Method Manager Crash", e);
-            }
-            throw e;
-        }
-    }
-
     /**
      * TODO(b/32343335): The entire systemRunning() method needs to be revisited.
      */
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/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
index c6f6fe2..fe91050 100644
--- a/services/core/java/com/android/server/notification/ManagedServices.java
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
@@ -1021,7 +1021,7 @@
         synchronized (mSnoozing) {
             mSnoozing.remove(user);
         }
-        rebindServices(true, user);
+        unbindUserServices(user);
     }
 
     public void onUserSwitched(int user) {
@@ -1408,12 +1408,24 @@
     void unbindOtherUserServices(int currentUser) {
         TimingsTraceAndSlog t = new TimingsTraceAndSlog();
         t.traceBegin("ManagedServices.unbindOtherUserServices_current" + currentUser);
-        final SparseArray<Set<ComponentName>> componentsToUnbind = new SparseArray<>();
+        unbindServicesImpl(currentUser, true /* allExceptUser */);
+        t.traceEnd();
+    }
 
+    void unbindUserServices(int user) {
+        TimingsTraceAndSlog t = new TimingsTraceAndSlog();
+        t.traceBegin("ManagedServices.unbindUserServices" + user);
+        unbindServicesImpl(user, false /* allExceptUser */);
+        t.traceEnd();
+    }
+
+    void unbindServicesImpl(int user, boolean allExceptUser) {
+        final SparseArray<Set<ComponentName>> componentsToUnbind = new SparseArray<>();
         synchronized (mMutex) {
             final Set<ManagedServiceInfo> removableBoundServices = getRemovableConnectedServices();
             for (ManagedServiceInfo info : removableBoundServices) {
-                if (info.userid != currentUser) {
+                if ((allExceptUser && (info.userid != user))
+                        || (!allExceptUser && (info.userid == user))) {
                     Set<ComponentName> toUnbind =
                             componentsToUnbind.get(info.userid, new ArraySet<>());
                     toUnbind.add(info.component);
@@ -1422,7 +1434,6 @@
             }
         }
         unbindFromServices(componentsToUnbind);
-        t.traceEnd();
     }
 
     protected void unbindFromServices(SparseArray<Set<ComponentName>> componentsToUnbind) {
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 802dfb1..a3c71c2 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -177,6 +177,7 @@
 import android.app.role.OnRoleHoldersChangedListener;
 import android.app.role.RoleManager;
 import android.app.usage.UsageEvents;
+import android.app.usage.UsageStatsManager;
 import android.app.usage.UsageStatsManagerInternal;
 import android.companion.ICompanionDeviceManager;
 import android.compat.annotation.ChangeId;
@@ -263,6 +264,7 @@
 import android.telephony.PhoneStateListener;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
+import android.text.format.DateUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.AtomicFile;
@@ -4159,7 +4161,7 @@
                 String pkg) {
             checkCallerIsSystemOrSameApp(pkg);
             return mPreferencesHelper.getNotificationChannelGroups(
-                    pkg, Binder.getCallingUid(), false, false, true);
+                    pkg, Binder.getCallingUid(), false, false, true, true, null);
         }
 
         @Override
@@ -4280,7 +4282,36 @@
                 String pkg, int uid, boolean includeDeleted) {
             enforceSystemOrSystemUI("getNotificationChannelGroupsForPackage");
             return mPreferencesHelper.getNotificationChannelGroups(
-                    pkg, uid, includeDeleted, true, false);
+                    pkg, uid, includeDeleted, true, false, true, null);
+        }
+
+        @Override
+        public ParceledListSlice<NotificationChannelGroup>
+                getRecentBlockedNotificationChannelGroupsForPackage(String pkg, int uid) {
+            enforceSystemOrSystemUI("getRecentBlockedNotificationChannelGroupsForPackage");
+            Set<String> recentlySentChannels = new HashSet<>();
+            long now = System.currentTimeMillis();
+            long startTime = now - (DateUtils.DAY_IN_MILLIS * 14);
+            UsageEvents events = mUsageStatsManagerInternal.queryEventsForUser(
+                UserHandle.getUserId(uid),  startTime, now, UsageEvents.SHOW_ALL_EVENT_DATA);
+            // get all channelids that sent notifs in the past 2 weeks
+            if (events != null) {
+                UsageEvents.Event event = new UsageEvents.Event();
+                while (events.hasNextEvent()) {
+                    events.getNextEvent(event);
+                    if (event.getEventType() == UsageEvents.Event.NOTIFICATION_INTERRUPTION) {
+                        if (pkg.equals(event.mPackage)) {
+                            String channelId = event.mNotificationChannelId;
+                            if (channelId != null) {
+                                recentlySentChannels.add(channelId);
+                            }
+                        }
+                    }
+                }
+            }
+
+            return mPreferencesHelper.getNotificationChannelGroups(
+                    pkg, uid, false, true, false, true, recentlySentChannels);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index b132a23..de698d9 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -1459,9 +1459,9 @@
         }
     }
 
-    @Override
     public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups(String pkg,
-            int uid, boolean includeDeleted, boolean includeNonGrouped, boolean includeEmpty) {
+            int uid, boolean includeDeleted, boolean includeNonGrouped, boolean includeEmpty,
+            boolean includeBlocked, Set<String> activeChannelFilter) {
         Objects.requireNonNull(pkg);
         Map<String, NotificationChannelGroup> groups = new ArrayMap<>();
         synchronized (mPackagePreferences) {
@@ -1473,7 +1473,11 @@
             int N = r.channels.size();
             for (int i = 0; i < N; i++) {
                 final NotificationChannel nc = r.channels.valueAt(i);
-                if (includeDeleted || !nc.isDeleted()) {
+                boolean includeChannel = (includeDeleted || !nc.isDeleted())
+                        && (activeChannelFilter == null
+                                || (includeBlocked && nc.getImportance() == IMPORTANCE_NONE)
+                                || activeChannelFilter.contains(nc.getId()));
+                if (includeChannel) {
                     if (nc.getGroup() != null) {
                         if (r.groups.get(nc.getGroup()) != null) {
                             NotificationChannelGroup ncg = groups.get(nc.getGroup());
@@ -1481,7 +1485,6 @@
                                 ncg = r.groups.get(nc.getGroup()).clone();
                                 ncg.setChannels(new ArrayList<>());
                                 groups.put(nc.getGroup(), ncg);
-
                             }
                             ncg.addChannel(nc);
                         }
diff --git a/services/core/java/com/android/server/notification/RankingConfig.java b/services/core/java/com/android/server/notification/RankingConfig.java
index fec3591..8df24c9 100644
--- a/services/core/java/com/android/server/notification/RankingConfig.java
+++ b/services/core/java/com/android/server/notification/RankingConfig.java
@@ -40,8 +40,6 @@
             int uid);
     void createNotificationChannelGroup(String pkg, int uid, NotificationChannelGroup group,
             boolean fromTargetApp, int callingUid, boolean isSystemOrSystemUi);
-    ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups(String pkg,
-            int uid, boolean includeDeleted, boolean includeNonGrouped, boolean includeEmpty);
     boolean createNotificationChannel(String pkg, int uid, NotificationChannel channel,
             boolean fromTargetApp, boolean hasDndAccess, int callingUid,
             boolean isSystemOrSystemUi);
diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java
index 52eef47..b9464d9 100644
--- a/services/core/java/com/android/server/om/OverlayManagerService.java
+++ b/services/core/java/com/android/server/om/OverlayManagerService.java
@@ -18,9 +18,6 @@
 
 import static android.app.AppGlobals.getPackageManager;
 import static android.content.Intent.ACTION_OVERLAY_CHANGED;
-import static android.content.Intent.ACTION_PACKAGE_ADDED;
-import static android.content.Intent.ACTION_PACKAGE_CHANGED;
-import static android.content.Intent.ACTION_PACKAGE_REMOVED;
 import static android.content.Intent.ACTION_USER_ADDED;
 import static android.content.Intent.ACTION_USER_REMOVED;
 import static android.content.Intent.EXTRA_PACKAGE_NAME;
@@ -31,10 +28,10 @@
 import static android.content.om.OverlayManagerTransaction.Request.TYPE_SET_ENABLED;
 import static android.content.om.OverlayManagerTransaction.Request.TYPE_UNREGISTER_FABRICATED;
 import static android.content.pm.PackageManager.SIGNATURE_MATCH;
+import static android.os.Process.INVALID_UID;
 import static android.os.Trace.TRACE_TAG_RRO;
 import static android.os.Trace.traceBegin;
 import static android.os.Trace.traceEnd;
-
 import static com.android.server.om.OverlayManagerServiceImpl.OperationFailedException;
 
 import android.annotation.NonNull;
@@ -82,6 +79,7 @@
 import android.util.Slog;
 import android.util.SparseArray;
 
+import com.android.internal.content.PackageMonitor;
 import com.android.internal.content.om.OverlayConfig;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.CollectionUtils;
@@ -261,6 +259,8 @@
 
     private final OverlayActorEnforcer mActorEnforcer;
 
+    private final PackageMonitor mPackageMonitor = new OverlayManagerPackageMonitor();
+
     private int mPrevStartedUserId = -1;
 
     public OverlayManagerService(@NonNull final Context context) {
@@ -277,16 +277,9 @@
                     OverlayConfig.getSystemInstance(), getDefaultOverlayPackages());
             mActorEnforcer = new OverlayActorEnforcer(mPackageManager);
 
-            HandlerThread packageReceiverThread = new HandlerThread(TAG);
-            packageReceiverThread.start();
-
-            final IntentFilter packageFilter = new IntentFilter();
-            packageFilter.addAction(ACTION_PACKAGE_ADDED);
-            packageFilter.addAction(ACTION_PACKAGE_CHANGED);
-            packageFilter.addAction(ACTION_PACKAGE_REMOVED);
-            packageFilter.addDataScheme("package");
-            getContext().registerReceiverAsUser(new PackageReceiver(), UserHandle.ALL,
-                    packageFilter, null, packageReceiverThread.getThreadHandler());
+            HandlerThread packageMonitorThread = new HandlerThread(TAG);
+            packageMonitorThread.start();
+            mPackageMonitor.register(context, packageMonitorThread.getLooper(), true);
 
             final IntentFilter userFilter = new IntentFilter();
             userFilter.addAction(ACTION_USER_ADDED);
@@ -372,166 +365,171 @@
         return defaultPackages.toArray(new String[defaultPackages.size()]);
     }
 
-    private final class PackageReceiver extends BroadcastReceiver {
+    private final class OverlayManagerPackageMonitor extends PackageMonitor {
+
         @Override
-        public void onReceive(@NonNull final Context context, @NonNull final Intent intent) {
-            final String action = intent.getAction();
-            if (action == null) {
-                Slog.e(TAG, "Cannot handle package broadcast with null action");
-                return;
-            }
-            final Uri data = intent.getData();
-            if (data == null) {
-                Slog.e(TAG, "Cannot handle package broadcast with null data");
-                return;
-            }
-            final String packageName = data.getSchemeSpecificPart();
-
-            final boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false);
-            final boolean systemUpdateUninstall =
-                    intent.getBooleanExtra(Intent.EXTRA_SYSTEM_UPDATE_UNINSTALL, false);
-
-            final int[] userIds;
-            final int extraUid = intent.getIntExtra(Intent.EXTRA_UID, UserHandle.USER_NULL);
-            if (extraUid == UserHandle.USER_NULL) {
-                userIds = mUserManager.getUserIds();
-            } else {
-                userIds = new int[] { UserHandle.getUserId(extraUid) };
-            }
-
-            switch (action) {
-                case ACTION_PACKAGE_ADDED:
-                    if (replacing) {
-                        onPackageReplaced(packageName, userIds);
-                    } else {
-                        onPackageAdded(packageName, userIds);
-                    }
-                    break;
-                case ACTION_PACKAGE_CHANGED:
-                    // ignore the intent if it was sent by the package manager as a result of the
-                    // overlay manager having sent ACTION_OVERLAY_CHANGED
-                    if (!ACTION_OVERLAY_CHANGED.equals(intent.getStringExtra(EXTRA_REASON))) {
-                        onPackageChanged(packageName, userIds);
-                    }
-                    break;
-                case ACTION_PACKAGE_REMOVED:
-                    if (replacing) {
-                        onPackageReplacing(packageName, systemUpdateUninstall, userIds);
-                    } else {
-                        onPackageRemoved(packageName, userIds);
-                    }
-                    break;
-                default:
-                    // do nothing
-                    break;
-            }
+        public void onPackageAppearedWithExtras(String packageName, Bundle extras) {
+            handlePackageAdd(packageName, extras);
         }
 
-        private void onPackageAdded(@NonNull final String packageName,
-                @NonNull final int[] userIds) {
-            try {
-                traceBegin(TRACE_TAG_RRO, "OMS#onPackageAdded " + packageName);
-                for (final int userId : userIds) {
-                    synchronized (mLock) {
-                        var packageState = mPackageManager.onPackageAdded(packageName, userId);
-                        if (packageState != null && !mPackageManager.isInstantApp(packageName,
-                                userId)) {
-                            try {
-                                updateTargetPackagesLocked(
-                                        mImpl.onPackageAdded(packageName, userId));
-                            } catch (OperationFailedException e) {
-                                Slog.e(TAG, "onPackageAdded internal error", e);
-                            }
+        @Override
+        public void onPackageChangedWithExtras(String packageName, Bundle extras) {
+            handlePackageChange(packageName, extras);
+        }
+
+        @Override
+        public void onPackageDisappearedWithExtras(String packageName, Bundle extras) {
+            handlePackageRemove(packageName, extras);
+        }
+    }
+
+    private int[] getUserIds(int uid) {
+        final int[] userIds;
+        if (uid == INVALID_UID) {
+            userIds = mUserManager.getUserIds();
+        } else {
+            userIds = new int[] { UserHandle.getUserId(uid) };
+        }
+        return userIds;
+    }
+
+    private void handlePackageAdd(String packageName, Bundle extras) {
+        final boolean replacing = extras.getBoolean(Intent.EXTRA_REPLACING, false);
+        final int uid = extras.getInt(Intent.EXTRA_UID, 0);
+        final int[] userIds = getUserIds(uid);
+        if (replacing) {
+            onPackageReplaced(packageName, userIds);
+        } else {
+            onPackageAdded(packageName, userIds);
+        }
+    }
+
+    private void handlePackageChange(String packageName, Bundle extras) {
+        final int uid = extras.getInt(Intent.EXTRA_UID, 0);
+        final int[] userIds = getUserIds(uid);
+        if (!ACTION_OVERLAY_CHANGED.equals(extras.getString(EXTRA_REASON))) {
+            onPackageChanged(packageName, userIds);
+        }
+    }
+
+    private void handlePackageRemove(String packageName, Bundle extras) {
+        final boolean replacing = extras.getBoolean(Intent.EXTRA_REPLACING, false);
+        final boolean systemUpdateUninstall =
+                extras.getBoolean(Intent.EXTRA_SYSTEM_UPDATE_UNINSTALL, false);
+        final int uid = extras.getInt(Intent.EXTRA_UID, 0);
+        final int[] userIds = getUserIds(uid);
+
+        if (replacing) {
+            onPackageReplacing(packageName, systemUpdateUninstall, userIds);
+        } else {
+            onPackageRemoved(packageName, userIds);
+        }
+    }
+
+    private void onPackageAdded(@NonNull final String packageName,
+            @NonNull final int[] userIds) {
+        try {
+            traceBegin(TRACE_TAG_RRO, "OMS#onPackageAdded " + packageName);
+            for (final int userId : userIds) {
+                synchronized (mLock) {
+                    var packageState = mPackageManager.onPackageAdded(packageName, userId);
+                    if (packageState != null && !mPackageManager.isInstantApp(packageName,
+                            userId)) {
+                        try {
+                            updateTargetPackagesLocked(
+                                    mImpl.onPackageAdded(packageName, userId));
+                        } catch (OperationFailedException e) {
+                            Slog.e(TAG, "onPackageAdded internal error", e);
                         }
                     }
                 }
-            } finally {
-                traceEnd(TRACE_TAG_RRO);
             }
+        } finally {
+            traceEnd(TRACE_TAG_RRO);
         }
+    }
 
-        private void onPackageChanged(@NonNull final String packageName,
-                @NonNull final int[] userIds) {
-            try {
-                traceBegin(TRACE_TAG_RRO, "OMS#onPackageChanged " + packageName);
-                for (int userId : userIds) {
-                    synchronized (mLock) {
-                        var packageState = mPackageManager.onPackageUpdated(packageName, userId);
-                        if (packageState != null && !mPackageManager.isInstantApp(packageName,
-                                userId)) {
-                            try {
-                                updateTargetPackagesLocked(
-                                        mImpl.onPackageChanged(packageName, userId));
-                            } catch (OperationFailedException e) {
-                                Slog.e(TAG, "onPackageChanged internal error", e);
-                            }
+    private void onPackageChanged(@NonNull final String packageName,
+            @NonNull final int[] userIds) {
+        try {
+            traceBegin(TRACE_TAG_RRO, "OMS#onPackageChanged " + packageName);
+            for (int userId : userIds) {
+                synchronized (mLock) {
+                    var packageState = mPackageManager.onPackageUpdated(packageName, userId);
+                    if (packageState != null && !mPackageManager.isInstantApp(packageName,
+                            userId)) {
+                        try {
+                            updateTargetPackagesLocked(
+                                    mImpl.onPackageChanged(packageName, userId));
+                        } catch (OperationFailedException e) {
+                            Slog.e(TAG, "onPackageChanged internal error", e);
                         }
                     }
                 }
-            } finally {
-                traceEnd(TRACE_TAG_RRO);
             }
+        } finally {
+            traceEnd(TRACE_TAG_RRO);
         }
+    }
 
-        private void onPackageReplacing(@NonNull final String packageName,
-                boolean systemUpdateUninstall, @NonNull final int[] userIds) {
-            try {
-                traceBegin(TRACE_TAG_RRO, "OMS#onPackageReplacing " + packageName);
-                for (int userId : userIds) {
-                    synchronized (mLock) {
-                        var packageState = mPackageManager.onPackageUpdated(packageName, userId);
-                        if (packageState != null && !mPackageManager.isInstantApp(packageName,
-                                userId)) {
-                            try {
-                                updateTargetPackagesLocked(mImpl.onPackageReplacing(packageName,
-                                        systemUpdateUninstall, userId));
-                            } catch (OperationFailedException e) {
-                                Slog.e(TAG, "onPackageReplacing internal error", e);
-                            }
+    private void onPackageReplacing(@NonNull final String packageName,
+            boolean systemUpdateUninstall, @NonNull final int[] userIds) {
+        try {
+            traceBegin(TRACE_TAG_RRO, "OMS#onPackageReplacing " + packageName);
+            for (int userId : userIds) {
+                synchronized (mLock) {
+                    var packageState = mPackageManager.onPackageUpdated(packageName, userId);
+                    if (packageState != null && !mPackageManager.isInstantApp(packageName,
+                            userId)) {
+                        try {
+                            updateTargetPackagesLocked(mImpl.onPackageReplacing(packageName,
+                                    systemUpdateUninstall, userId));
+                        } catch (OperationFailedException e) {
+                            Slog.e(TAG, "onPackageReplacing internal error", e);
                         }
                     }
                 }
-            } finally {
-                traceEnd(TRACE_TAG_RRO);
             }
+        } finally {
+            traceEnd(TRACE_TAG_RRO);
         }
+    }
 
-        private void onPackageReplaced(@NonNull final String packageName,
-                @NonNull final int[] userIds) {
-            try {
-                traceBegin(TRACE_TAG_RRO, "OMS#onPackageReplaced " + packageName);
-                for (int userId : userIds) {
-                    synchronized (mLock) {
-                        var packageState = mPackageManager.onPackageUpdated(packageName, userId);
-                        if (packageState != null && !mPackageManager.isInstantApp(packageName,
-                                userId)) {
-                            try {
-                                updateTargetPackagesLocked(
-                                        mImpl.onPackageReplaced(packageName, userId));
-                            } catch (OperationFailedException e) {
-                                Slog.e(TAG, "onPackageReplaced internal error", e);
-                            }
+    private void onPackageReplaced(@NonNull final String packageName,
+            @NonNull final int[] userIds) {
+        try {
+            traceBegin(TRACE_TAG_RRO, "OMS#onPackageReplaced " + packageName);
+            for (int userId : userIds) {
+                synchronized (mLock) {
+                    var packageState = mPackageManager.onPackageUpdated(packageName, userId);
+                    if (packageState != null && !mPackageManager.isInstantApp(packageName,
+                            userId)) {
+                        try {
+                            updateTargetPackagesLocked(
+                                    mImpl.onPackageReplaced(packageName, userId));
+                        } catch (OperationFailedException e) {
+                            Slog.e(TAG, "onPackageReplaced internal error", e);
                         }
                     }
                 }
-            } finally {
-                traceEnd(TRACE_TAG_RRO);
             }
+        } finally {
+            traceEnd(TRACE_TAG_RRO);
         }
+    }
 
-        private void onPackageRemoved(@NonNull final String packageName,
-                @NonNull final int[] userIds) {
-            try {
-                traceBegin(TRACE_TAG_RRO, "OMS#onPackageRemoved " + packageName);
-                for (int userId : userIds) {
-                    synchronized (mLock) {
-                        mPackageManager.onPackageRemoved(packageName, userId);
-                        updateTargetPackagesLocked(mImpl.onPackageRemoved(packageName, userId));
-                    }
+    private void onPackageRemoved(@NonNull final String packageName,
+            @NonNull final int[] userIds) {
+        try {
+            traceBegin(TRACE_TAG_RRO, "OMS#onPackageRemoved " + packageName);
+            for (int userId : userIds) {
+                synchronized (mLock) {
+                    mPackageManager.onPackageRemoved(packageName, userId);
+                    updateTargetPackagesLocked(mImpl.onPackageRemoved(packageName, userId));
                 }
-            } finally {
-                traceEnd(TRACE_TAG_RRO);
             }
+        } finally {
+            traceEnd(TRACE_TAG_RRO);
         }
     }
 
@@ -684,7 +682,7 @@
                     synchronized (mLock) {
                         try {
                             mImpl.setEnabledExclusive(
-                                    overlay, false /* withinCategory */, realUserId)
+                                            overlay, false /* withinCategory */, realUserId)
                                     .ifPresent(
                                             OverlayManagerService.this::updateTargetPackagesLocked);
                             return true;
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 96d8a54..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) {
@@ -2931,15 +2930,40 @@
      * @return a future that will be completed when the whole process is completed.
      */
     private CompletableFuture<Void> install() {
+        // `futures` either contains only one session (`this`) or contains one parent session
+        // (`this`) and n-1 child sessions.
         List<CompletableFuture<InstallResult>> futures = installNonStaged();
         CompletableFuture<InstallResult>[] arr = new CompletableFuture[futures.size()];
         return CompletableFuture.allOf(futures.toArray(arr)).whenComplete((r, t) -> {
             if (t == null) {
                 setSessionApplied();
+                var multiPackageWarnings = new ArrayList<String>();
+                if (isMultiPackage()) {
+                    // This is a parent session. Collect warnings from children.
+                    for (CompletableFuture<InstallResult> f : futures) {
+                        InstallResult result = f.join();
+                        if (result.session != this && result.extras != null) {
+                            ArrayList<String> childWarnings = result.extras.getStringArrayList(
+                                    PackageInstaller.EXTRA_WARNINGS);
+                            if (!ArrayUtils.isEmpty(childWarnings)) {
+                                multiPackageWarnings.addAll(childWarnings);
+                            }
+                        }
+                    }
+                }
                 for (CompletableFuture<InstallResult> f : futures) {
                     InstallResult result = f.join();
+                    Bundle extras = result.extras;
+                    if (isMultiPackage() && result.session == this
+                            && !multiPackageWarnings.isEmpty()) {
+                        if (extras == null) {
+                            extras = new Bundle();
+                        }
+                        extras.putStringArrayList(
+                                PackageInstaller.EXTRA_WARNINGS, multiPackageWarnings);
+                    }
                     result.session.dispatchSessionFinished(
-                            INSTALL_SUCCEEDED, "Session installed", result.extras);
+                            INSTALL_SUCCEEDED, "Session installed", extras);
                 }
             } else {
                 PackageManagerException e = (PackageManagerException) t.getCause();
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 dc75a98..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;
@@ -1983,7 +1983,6 @@
             public void run() {
                 if (mPendingHomeKeyEvent != null) {
                     handleShortPressOnHome(mPendingHomeKeyEvent);
-                    mPendingHomeKeyEvent.recycle();
                     mPendingHomeKeyEvent = null;
                 }
             }
@@ -2028,7 +2027,7 @@
                     if (mDoubleTapOnHomeBehavior != DOUBLE_TAP_HOME_PIP_MENU
                             || mPictureInPictureVisible) {
                         mHandler.removeCallbacks(mHomeDoubleTapTimeoutRunnable); // just in case
-                        mPendingHomeKeyEvent = KeyEvent.obtain(event);
+                        mPendingHomeKeyEvent = event;
                         mHandler.postDelayed(mHomeDoubleTapTimeoutRunnable,
                                 ViewConfiguration.getDoubleTapTimeout());
                         return true;
@@ -2036,11 +2035,7 @@
                 }
 
                 // Post to main thread to avoid blocking input pipeline.
-                final KeyEvent shortPressEvent = KeyEvent.obtain(event);
-                mHandler.post(() -> {
-                    handleShortPressOnHome(shortPressEvent);
-                    shortPressEvent.recycle();
-                });
+                mHandler.post(() -> handleShortPressOnHome(event));
                 return true;
             }
 
@@ -2067,14 +2062,9 @@
             if (repeatCount == 0) {
                 mHomePressed = true;
                 if (mPendingHomeKeyEvent != null) {
-                    mPendingHomeKeyEvent.recycle();
                     mPendingHomeKeyEvent = null;
                     mHandler.removeCallbacks(mHomeDoubleTapTimeoutRunnable);
-                    final KeyEvent doublePressEvent = KeyEvent.obtain(event);
-                    mHandler.post(() -> {
-                        handleDoubleTapOnHome(doublePressEvent);
-                        doublePressEvent.recycle();
-                    });
+                    mHandler.post(() -> handleDoubleTapOnHome(event));
                 // TODO(multi-display): Remove display id check once we support recents on
                 // multi-display
                 } else if (mDoubleTapOnHomeBehavior == DOUBLE_TAP_HOME_RECENT_SYSTEM_UI
@@ -2084,11 +2074,7 @@
             } else if ((event.getFlags() & KeyEvent.FLAG_LONG_PRESS) != 0) {
                 if (!keyguardOn) {
                     // Post to main thread to avoid blocking input pipeline.
-                    final KeyEvent longPressEvent = KeyEvent.obtain(event);
-                    mHandler.post(() -> {
-                        handleLongPressOnHome(longPressEvent);
-                        longPressEvent.recycle();
-                    });
+                    mHandler.post(() -> handleLongPressOnHome(event));
                 }
             }
             return true;
diff --git a/services/core/java/com/android/server/speech/RemoteSpeechRecognitionService.java b/services/core/java/com/android/server/speech/RemoteSpeechRecognitionService.java
index 96f4a01..c2666f6 100644
--- a/services/core/java/com/android/server/speech/RemoteSpeechRecognitionService.java
+++ b/services/core/java/com/android/server/speech/RemoteSpeechRecognitionService.java
@@ -297,7 +297,8 @@
                     return;
                 }
 
-                for (ClientState clientState : mClients.values()) {
+
+                for (ClientState clientState : mClients.values().toArray(new ClientState[0])) {
                     tryRespondWithError(
                             clientState.mDelegatingListener.mRemoteListener,
                             SpeechRecognizer.ERROR_SERVER_DISCONNECTED);
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/AsyncRotationController.java b/services/core/java/com/android/server/wm/AsyncRotationController.java
index 4b463c7..b87e761 100644
--- a/services/core/java/com/android/server/wm/AsyncRotationController.java
+++ b/services/core/java/com/android/server/wm/AsyncRotationController.java
@@ -275,15 +275,18 @@
             // The previous animation leash will be dropped when preparing fade-in animation, so
             // simply apply new animation without restoring the transformation.
             fadeWindowToken(true /* show */, windowToken, ANIMATION_TYPE_TOKEN_TRANSFORM);
-        } else if (op.mAction == Operation.ACTION_SEAMLESS
-                && op.mLeash != null && op.mLeash.isValid()) {
+        } else if (op.isValidSeamless()) {
             if (DEBUG) Slog.d(TAG, "finishOp undo seamless " + windowToken.getTopChild());
             final SurfaceControl.Transaction t = windowToken.getSyncTransaction();
-            t.setMatrix(op.mLeash, 1, 0, 0, 1);
-            t.setPosition(op.mLeash, 0, 0);
+            clearTransform(t, op.mLeash);
         }
     }
 
+    private static void clearTransform(SurfaceControl.Transaction t, SurfaceControl sc) {
+        t.setMatrix(sc, 1, 0, 0, 1);
+        t.setPosition(sc, 0, 0);
+    }
+
     /**
      * Completes all operations such as applying fade-in animation on the previously hidden window
      * tokens. This is called if all windows are ready in new rotation or timed out.
@@ -400,7 +403,19 @@
                 synchronized (mService.mGlobalLock) {
                     Slog.i(TAG, "Async rotation timeout: " + (!mIsStartTransactionCommitted
                             ? " start transaction is not committed" : mTargetWindowTokens));
-                    mIsStartTransactionCommitted = true;
+                    if (!mIsStartTransactionCommitted) {
+                        // The transaction commit timeout will be handled by:
+                        // 1. BLASTSyncEngine will notify onTransactionCommitTimeout() and then
+                        //    apply the start transaction of transition.
+                        // 2. The TransactionCommittedListener in setupStartTransaction() will be
+                        //    notified to finish the operations of mTargetWindowTokens.
+                        // 3. The slow remote side will also apply the start transaction which may
+                        //    contain stale surface transform.
+                        // 4. Finally, the slow remote reports transition finished. The cleanup
+                        //    transaction from step (1) will be applied when finishing transition,
+                        //    which will recover the stale state from (3).
+                        return;
+                    }
                     mDisplayContent.finishAsyncRotationIfPossible();
                     mService.mWindowPlacerLocked.performSurfacePlacement();
                 }
@@ -539,6 +554,20 @@
         });
     }
 
+    /** Called when the start transition is ready, but it is not applied in time. */
+    void onTransactionCommitTimeout(SurfaceControl.Transaction t) {
+        if (mIsStartTransactionCommitted) return;
+        for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) {
+            final Operation op = mTargetWindowTokens.valueAt(i);
+            op.mIsCompletionPending = true;
+            if (op.isValidSeamless()) {
+                Slog.d(TAG, "Transaction timeout. Clear transform for "
+                        + mTargetWindowTokens.keyAt(i).getTopChild());
+                clearTransform(t, op.mLeash);
+            }
+        }
+    }
+
     /** Called when the transition by shell is done. */
     void onTransitionFinished() {
         if (mTransitionOp == OP_CHANGE) {
@@ -681,6 +710,10 @@
             mAction = action;
         }
 
+        boolean isValidSeamless() {
+            return mAction == ACTION_SEAMLESS && mLeash != null && mLeash.isValid();
+        }
+
         @Override
         public String toString() {
             return "Operation{a=" + mAction + " pending=" + mIsCompletionPending + '}';
diff --git a/services/core/java/com/android/server/wm/BLASTSyncEngine.java b/services/core/java/com/android/server/wm/BLASTSyncEngine.java
index 98ee98b..4444709 100644
--- a/services/core/java/com/android/server/wm/BLASTSyncEngine.java
+++ b/services/core/java/com/android/server/wm/BLASTSyncEngine.java
@@ -95,6 +95,7 @@
 
     interface TransactionReadyListener {
         void onTransactionReady(int mSyncId, SurfaceControl.Transaction transaction);
+        default void onTransactionCommitTimeout() {}
     }
 
     /**
@@ -249,6 +250,7 @@
                            " commit callback. Application ANR likely to follow.");
                     Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
                     synchronized (mWm.mGlobalLock) {
+                        mListener.onTransactionCommitTimeout();
                         onCommitted(merged.mNativeObject != 0
                                 ? merged : mWm.mTransactionFactory.get());
                     }
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index 4bafccd..e97bda2 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -418,7 +418,7 @@
             callerAppUid = realCallingUid;
         }
         // don't abort if the callerApp or other processes of that uid are allowed in any way
-        if (callerApp != null && useCallingUidState) {
+        if (callerApp != null && (useCallingUidState || callerAppBasedOnPiSender)) {
             // first check the original calling process
             final @BalCode int balAllowedForCaller = callerApp
                     .areBackgroundActivityStartsAllowed(appSwitchState);
@@ -509,6 +509,7 @@
                 + "; intent: " + intent
                 + "; callerApp: " + callerApp
                 + "; inVisibleTask: " + (callerApp != null && callerApp.hasActivityInVisibleTask())
+                + "; resultIfPiSenderAllowsBal: " + balCodeToString(resultIfPiSenderAllowsBal)
                 + "]";
         if (resultIfPiSenderAllowsBal != BAL_BLOCK) {
             // We should have returned before if !logVerdictChangeByPiDefaultChange
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index d461d1e..a1b8949 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -130,6 +130,7 @@
     private final int mUndockedHdmiRotation;
     private final RotationAnimationPair mTmpRotationAnim = new RotationAnimationPair();
     private final RotationHistory mRotationHistory = new RotationHistory();
+    private final RotationLockHistory mRotationLockHistory = new RotationLockHistory();
 
     private OrientationListener mOrientationListener;
     private StatusBarManagerInternal mStatusBarManagerInternal;
@@ -922,7 +923,8 @@
     }
 
     @VisibleForTesting
-    void setUserRotation(int userRotationMode, int userRotation) {
+    void setUserRotation(int userRotationMode, int userRotation, String caller) {
+        mRotationLockHistory.addRecord(userRotationMode, userRotation, caller);
         mRotationChoiceShownToUserForConfirmation = ROTATION_UNDEFINED;
         if (useDefaultSettingsProvider()) {
             // We'll be notified via settings listener, so we don't need to update internal values.
@@ -953,17 +955,17 @@
         }
     }
 
-    void freezeRotation(int rotation) {
+    void freezeRotation(int rotation, String caller) {
         if (mDeviceStateController.shouldReverseRotationDirectionAroundZAxis(mDisplayContent)) {
             rotation = RotationUtils.reverseRotationDirectionAroundZAxis(rotation);
         }
 
         rotation = (rotation == -1) ? mRotation : rotation;
-        setUserRotation(WindowManagerPolicy.USER_ROTATION_LOCKED, rotation);
+        setUserRotation(WindowManagerPolicy.USER_ROTATION_LOCKED, rotation, caller);
     }
 
-    void thawRotation() {
-        setUserRotation(WindowManagerPolicy.USER_ROTATION_FREE, mUserRotation);
+    void thawRotation(String caller) {
+        setUserRotation(WindowManagerPolicy.USER_ROTATION_FREE, mUserRotation, caller);
     }
 
     boolean isRotationFrozen() {
@@ -1712,6 +1714,15 @@
                 r.dump(prefix, pw);
             }
         }
+
+        if (!mRotationLockHistory.mRecords.isEmpty()) {
+            pw.println();
+            pw.println(prefix + "  RotationLockHistory");
+            prefix = "    " + prefix;
+            for (RotationLockHistory.Record r : mRotationLockHistory.mRecords) {
+                r.dump(prefix, pw);
+            }
+        }
     }
 
     void dumpDebug(ProtoOutputStream proto, long fieldId) {
@@ -2133,6 +2144,40 @@
         }
     }
 
+    private static class RotationLockHistory {
+        private static final int MAX_SIZE = 8;
+
+        private static class Record {
+            @WindowManagerPolicy.UserRotationMode final int mUserRotationMode;
+            @Surface.Rotation final int mUserRotation;
+            final String mCaller;
+            final long mTimestamp = System.currentTimeMillis();
+
+            private Record(int userRotationMode, int userRotation, String caller) {
+                mUserRotationMode = userRotationMode;
+                mUserRotation = userRotation;
+                mCaller = caller;
+            }
+
+            void dump(String prefix, PrintWriter pw) {
+                pw.println(prefix + TimeUtils.logTimeOfDay(mTimestamp) + ": "
+                        + "mode="  + WindowManagerPolicy.userRotationModeToString(mUserRotationMode)
+                        + ", rotation=" + Surface.rotationToString(mUserRotation)
+                        + ", caller=" + mCaller);
+            }
+        }
+
+        private final ArrayDeque<RotationLockHistory.Record> mRecords = new ArrayDeque<>(MAX_SIZE);
+
+        void addRecord(@WindowManagerPolicy.UserRotationMode int userRotationMode,
+                @Surface.Rotation int userRotation, String caller) {
+            if (mRecords.size() >= MAX_SIZE) {
+                mRecords.removeFirst();
+            }
+            mRecords.addLast(new Record(userRotationMode, userRotation, caller));
+        }
+    }
+
     private static class RotationHistory {
         private static final int MAX_SIZE = 8;
         private static final int NO_FOLD_CONTROLLER = -2;
diff --git a/services/core/java/com/android/server/wm/DisplayRotationReversionController.java b/services/core/java/com/android/server/wm/DisplayRotationReversionController.java
index d3a8a82..4eb2d88 100644
--- a/services/core/java/com/android/server/wm/DisplayRotationReversionController.java
+++ b/services/core/java/com/android/server/wm/DisplayRotationReversionController.java
@@ -118,7 +118,8 @@
         if (mDisplayContent.getDisplayRotation().isRotationFrozen()) {
             mDisplayContent.getDisplayRotation().setUserRotation(
                     mUserRotationModeOverridden,
-                    mUserRotationOverridden);
+                    mUserRotationOverridden,
+                    /* caller= */ "DisplayRotationReversionController#revertOverride");
             return true;
         } else {
             return false;
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/Session.java b/services/core/java/com/android/server/wm/Session.java
index 0674ec1..bbe44c5 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -42,6 +42,7 @@
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_TASK_POSITIONING;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.PendingIntent;
 import android.content.ClipData;
@@ -100,6 +101,8 @@
     final IWindowSessionCallback mCallback;
     final int mUid;
     final int mPid;
+    @NonNull
+    final WindowProcessController mProcess;
     private final String mStringName;
     SurfaceSession mSurfaceSession;
     private int mNumWindow = 0;
@@ -126,11 +129,23 @@
     final boolean mSetsUnrestrictedKeepClearAreas;
 
     public Session(WindowManagerService service, IWindowSessionCallback callback) {
+        this(service, callback, Binder.getCallingPid(), Binder.getCallingUid());
+    }
+
+    @VisibleForTesting
+    Session(WindowManagerService service, IWindowSessionCallback callback,
+            int callingPid, int callingUid) {
         mService = service;
         mCallback = callback;
-        mUid = Binder.getCallingUid();
-        mPid = Binder.getCallingPid();
-        mLastReportedAnimatorScale = service.getCurrentAnimatorScale();
+        mPid = callingPid;
+        mUid = callingUid;
+        synchronized (service.mGlobalLock) {
+            mLastReportedAnimatorScale = service.getCurrentAnimatorScale();
+            mProcess = service.mAtmService.mProcessMap.getProcess(mPid);
+        }
+        if (mProcess == null) {
+            throw new IllegalStateException("Unknown pid=" + mPid + " uid=" + mUid);
+        }
         mCanAddInternalSystemWindow = service.mContext.checkCallingOrSelfPermission(
                 INTERNAL_SYSTEM_WINDOW) == PERMISSION_GRANTED;
         mCanForceShowingInsets = service.mAtmService.isCallerRecents(mUid)
@@ -715,13 +730,8 @@
 
     void windowAddedLocked() {
         if (mPackageName == null) {
-            final WindowProcessController wpc = mService.mAtmService.mProcessMap.getProcess(mPid);
-            if (wpc != null) {
-                mPackageName = wpc.mInfo.packageName;
-                mRelayoutTag = "relayoutWindow: " + mPackageName;
-            } else {
-                Slog.e(TAG_WM, "Unknown process pid=" + mPid);
-            }
+            mPackageName = mProcess.mInfo.packageName;
+            mRelayoutTag = "relayoutWindow: " + mPackageName;
         }
         if (mSurfaceSession == null) {
             if (DEBUG) {
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index bbafa25..fac98b8 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1683,6 +1683,18 @@
         }
     }
 
+    @Override
+    public void onTransactionCommitTimeout() {
+        if (mCleanupTransaction == null) return;
+        for (int i = mTargetDisplays.size() - 1; i >= 0; --i) {
+            final DisplayContent dc = mTargetDisplays.get(i);
+            final AsyncRotationController asyncRotationController = dc.getAsyncRotationController();
+            if (asyncRotationController != null && containsChangeFor(dc, mTargets)) {
+                asyncRotationController.onTransactionCommitTimeout(mCleanupTransaction);
+            }
+        }
+    }
+
     /**
      * Collect tasks which moved-to-top as part of this transition. This also updates the
      * controller's latest-reported when relevant.
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 9a1920d..3faf8b9 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -4281,8 +4281,8 @@
     }
 
     @Override
-    public void freezeRotation(int rotation) {
-        freezeDisplayRotation(Display.DEFAULT_DISPLAY, rotation);
+    public void freezeRotation(int rotation, String caller) {
+        freezeDisplayRotation(Display.DEFAULT_DISPLAY, rotation, caller);
     }
 
     /**
@@ -4292,7 +4292,7 @@
      * @param rotation The desired rotation to freeze to, or -1 to use the current rotation.
      */
     @Override
-    public void freezeDisplayRotation(int displayId, int rotation) {
+    public void freezeDisplayRotation(int displayId, int rotation, String caller) {
         // TODO(multi-display): Track which display is rotated.
         if (!checkCallingPermission(android.Manifest.permission.SET_ORIENTATION,
                 "freezeRotation()")) {
@@ -4302,6 +4302,9 @@
             throw new IllegalArgumentException("Rotation argument must be -1 or a valid "
                     + "rotation constant.");
         }
+        ProtoLog.v(WM_DEBUG_ORIENTATION,
+                "freezeDisplayRotation: current rotation=%d, new rotation=%d, caller=%s",
+                getDefaultDisplayRotation(), rotation, caller);
 
         final long origId = Binder.clearCallingIdentity();
         try {
@@ -4311,7 +4314,7 @@
                     Slog.w(TAG, "Trying to freeze rotation for a missing display.");
                     return;
                 }
-                display.getDisplayRotation().freezeRotation(rotation);
+                display.getDisplayRotation().freezeRotation(rotation, caller);
             }
         } finally {
             Binder.restoreCallingIdentity(origId);
@@ -4321,8 +4324,8 @@
     }
 
     @Override
-    public void thawRotation() {
-        thawDisplayRotation(Display.DEFAULT_DISPLAY);
+    public void thawRotation(String caller) {
+        thawDisplayRotation(Display.DEFAULT_DISPLAY, caller);
     }
 
     /**
@@ -4330,13 +4333,14 @@
      * Persists across reboots.
      */
     @Override
-    public void thawDisplayRotation(int displayId) {
+    public void thawDisplayRotation(int displayId, String caller) {
         if (!checkCallingPermission(android.Manifest.permission.SET_ORIENTATION,
                 "thawRotation()")) {
             throw new SecurityException("Requires SET_ORIENTATION permission");
         }
 
-        ProtoLog.v(WM_DEBUG_ORIENTATION, "thawRotation: mRotation=%d", getDefaultDisplayRotation());
+        ProtoLog.v(WM_DEBUG_ORIENTATION, "thawRotation: mRotation=%d, caller=%s",
+                getDefaultDisplayRotation(), caller);
 
         final long origId = Binder.clearCallingIdentity();
         try {
@@ -4346,7 +4350,7 @@
                     Slog.w(TAG, "Trying to thaw rotation for a missing display.");
                     return;
                 }
-                display.getDisplayRotation().thawRotation();
+                display.getDisplayRotation().thawRotation(caller);
             }
         } finally {
             Binder.restoreCallingIdentity(origId);
@@ -5237,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() {
@@ -5254,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));
@@ -9643,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 74a0bafd3..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);
@@ -438,7 +445,8 @@
         }
 
         if ("free".equals(lockMode)) {
-            mInternal.thawDisplayRotation(displayId);
+            mInternal.thawDisplayRotation(displayId,
+                    /* caller= */ "WindowManagerShellCommand#free");
             return 0;
         }
 
@@ -451,7 +459,8 @@
         try {
             final int rotation =
                     arg != null ? Integer.parseInt(arg) : -1 /* lock to current rotation */;
-            mInternal.freezeDisplayRotation(displayId, rotation);
+            mInternal.freezeDisplayRotation(displayId, rotation,
+                    /* caller= */ "WindowManagerShellCommand#lock");
             return 0;
         } catch (IllegalArgumentException e) {
             getErrPrintWriter().println("Error: " + e.getMessage());
@@ -1433,7 +1442,8 @@
         mInterface.setForcedDisplayScalingMode(displayId, DisplayContent.FORCE_SCALING_MODE_AUTO);
 
         // user-rotation
-        mInternal.thawDisplayRotation(displayId);
+        mInternal.thawDisplayRotation(displayId,
+                /* caller= */ "WindowManagerShellCommand#runReset");
 
         // fixed-to-user-rotation
         mInterface.setFixedToUserRotation(displayId, IWindowManager.FIXED_TO_USER_ROTATION_DEFAULT);
@@ -1489,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/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index b12cc0b..ebef606 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -754,8 +754,6 @@
 
     static final int BLAST_TIMEOUT_DURATION = 5000; /* milliseconds */
 
-    private final WindowProcessController mWpcForDisplayAreaConfigChanges;
-
     class DrawHandler {
         Consumer<SurfaceControl.Transaction> mConsumer;
         int mSeqId;
@@ -1129,7 +1127,6 @@
             mBaseLayer = 0;
             mSubLayer = 0;
             mWinAnimator = null;
-            mWpcForDisplayAreaConfigChanges = null;
             mOverrideScale = 1f;
             return;
         }
@@ -1186,11 +1183,6 @@
             ProtoLog.v(WM_DEBUG_ADD_REMOVE, "Adding %s to %s", this, parentWindow);
             parentWindow.addChild(this, sWindowSubLayerComparator);
         }
-
-        // System process or invalid process cannot register to display area config change.
-        mWpcForDisplayAreaConfigChanges = (s.mPid == MY_PID || s.mPid < 0)
-                ? null
-                : service.mAtmService.getProcessController(s.mPid, s.mUid);
     }
 
     boolean shouldWindowHandleBeTrusted(Session s) {
@@ -3621,14 +3613,17 @@
     /** @return {@code true} if the process registered to a display area as a config listener. */
     private boolean registeredForDisplayAreaConfigChanges() {
         final WindowState parentWindow = getParentWindow();
-        final WindowProcessController wpc = parentWindow != null
-                ? parentWindow.mWpcForDisplayAreaConfigChanges
-                : mWpcForDisplayAreaConfigChanges;
-        return wpc != null && wpc.registeredForDisplayAreaConfigChanges();
+        final Session session = parentWindow != null ? parentWindow.mSession : mSession;
+        if (session.mPid == MY_PID) {
+            // System process cannot register to display area config change.
+            return false;
+        }
+        return session.mProcess.registeredForDisplayAreaConfigChanges();
     }
 
+    @NonNull
     WindowProcessController getProcess() {
-        return mWpcForDisplayAreaConfigChanges;
+        return mSession.mProcess;
     }
 
     /**
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/foldables/devicestateprovider/Android.bp b/services/foldables/devicestateprovider/Android.bp
new file mode 100644
index 0000000..34737ef
--- /dev/null
+++ b/services/foldables/devicestateprovider/Android.bp
@@ -0,0 +1,13 @@
+package {
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_library {
+    name: "foldable-device-state-provider",
+    srcs: [
+        "src/**/*.java"
+    ],
+    libs: [
+        "services",
+    ],
+}
diff --git a/services/foldables/devicestateprovider/OWNERS b/services/foldables/devicestateprovider/OWNERS
index b2dcd0c..5732844 100644
--- a/services/foldables/devicestateprovider/OWNERS
+++ b/services/foldables/devicestateprovider/OWNERS
@@ -1,6 +1,6 @@
 akulian@google.com
-kennethford@google.com
 jiamingliu@google.com
 kchyn@google.com
+kennethford@google.com
 nickchameyev@google.com
 nicomazz@google.com
\ No newline at end of file
diff --git a/services/foldables/devicestateprovider/README.md b/services/foldables/devicestateprovider/README.md
new file mode 100644
index 0000000..90174c0
--- /dev/null
+++ b/services/foldables/devicestateprovider/README.md
@@ -0,0 +1,3 @@
+# Foldable Device State Provider library
+
+This library provides foldable-specific classes that could be used to implement a custom DeviceStateProvider.
\ No newline at end of file
diff --git a/services/foldables/devicestateprovider/TEST_MAPPING b/services/foldables/devicestateprovider/TEST_MAPPING
new file mode 100644
index 0000000..47de131
--- /dev/null
+++ b/services/foldables/devicestateprovider/TEST_MAPPING
@@ -0,0 +1,12 @@
+{
+  "presubmit": [
+    {
+      "name": "foldable-device-state-provider-tests",
+      "options": [
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
+    }
+  ]
+}
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
new file mode 100644
index 0000000..aea46d1
--- /dev/null
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
@@ -0,0 +1,537 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.policy;
+
+import static android.hardware.SensorManager.SENSOR_DELAY_FASTEST;
+import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
+import static android.hardware.devicestate.DeviceStateManager.MAXIMUM_DEVICE_STATE;
+import static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STATE;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.PowerManager;
+import android.hardware.display.DisplayManager;
+import android.os.Trace;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.view.Display;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+import com.android.server.devicestate.DeviceState;
+import com.android.server.devicestate.DeviceStateProvider;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.function.BooleanSupplier;
+import java.util.function.Function;
+
+/**
+ * Device state provider for foldable devices.
+ *
+ * It is an implementation of {@link DeviceStateProvider} tailored specifically for
+ * foldable devices and allows simple callback-based configuration with hall sensor
+ * and hinge angle sensor values.
+ */
+public final class FoldableDeviceStateProvider implements DeviceStateProvider,
+        SensorEventListener, PowerManager.OnThermalStatusChangedListener,
+       DisplayManager.DisplayListener  {
+
+    private static final String TAG = "FoldableDeviceStateProvider";
+    private static final boolean DEBUG = false;
+
+    // Lock for internal state.
+    private final Object mLock = new Object();
+
+    // List of supported states in ascending order based on their identifier.
+    private final DeviceState[] mOrderedStates;
+
+    // Map of state identifier to a boolean supplier that returns true when all required conditions
+    // are met for the device to be in the state.
+    private final SparseArray<BooleanSupplier> mStateConditions = new SparseArray<>();
+
+    private final Sensor mHingeAngleSensor;
+    private final DisplayManager mDisplayManager;
+    private final Sensor mHallSensor;
+
+    @Nullable
+    @GuardedBy("mLock")
+    private Listener mListener = null;
+    @GuardedBy("mLock")
+    private int mLastReportedState = INVALID_DEVICE_STATE;
+    @GuardedBy("mLock")
+    private SensorEvent mLastHingeAngleSensorEvent = null;
+    @GuardedBy("mLock")
+    private SensorEvent mLastHallSensorEvent = null;
+    @GuardedBy("mLock")
+    private @PowerManager.ThermalStatus
+    int mThermalStatus = PowerManager.THERMAL_STATUS_NONE;
+    @GuardedBy("mLock")
+    private boolean mIsScreenOn = false;
+
+    @GuardedBy("mLock")
+    private boolean mPowerSaveModeEnabled;
+
+    public FoldableDeviceStateProvider(@NonNull Context context,
+            @NonNull SensorManager sensorManager,
+            @NonNull Sensor hingeAngleSensor,
+            @NonNull Sensor hallSensor,
+            @NonNull DisplayManager displayManager,
+            @NonNull DeviceStateConfiguration[] deviceStateConfigurations) {
+
+        Preconditions.checkArgument(deviceStateConfigurations.length > 0,
+                "Device state configurations array must not be empty");
+
+        mHingeAngleSensor = hingeAngleSensor;
+        mHallSensor = hallSensor;
+        mDisplayManager = displayManager;
+
+        sensorManager.registerListener(this, mHingeAngleSensor, SENSOR_DELAY_FASTEST);
+        sensorManager.registerListener(this, mHallSensor, SENSOR_DELAY_FASTEST);
+
+        mOrderedStates = new DeviceState[deviceStateConfigurations.length];
+        for (int i = 0; i < deviceStateConfigurations.length; i++) {
+            final DeviceStateConfiguration configuration = deviceStateConfigurations[i];
+            mOrderedStates[i] = configuration.mDeviceState;
+
+            if (mStateConditions.get(configuration.mDeviceState.getIdentifier()) != null) {
+                throw new IllegalArgumentException("Device state configurations must have unique"
+                        + " device state identifiers, found duplicated identifier: " +
+                        configuration.mDeviceState.getIdentifier());
+            }
+
+            mStateConditions.put(configuration.mDeviceState.getIdentifier(), () ->
+                    configuration.mPredicate.apply(this));
+        }
+
+        mDisplayManager.registerDisplayListener(
+                /* listener = */ this,
+                /* handler= */ null,
+                /* eventsMask= */ DisplayManager.EVENT_FLAG_DISPLAY_CHANGED);
+
+        Arrays.sort(mOrderedStates, Comparator.comparingInt(DeviceState::getIdentifier));
+
+        PowerManager powerManager = context.getSystemService(PowerManager.class);
+        if (powerManager != null) {
+            // If any of the device states are thermal sensitive, i.e. it should be disabled when
+            // the device is overheating, then we will update the list of supported states when
+            // thermal status changes.
+            if (hasThermalSensitiveState(deviceStateConfigurations)) {
+                powerManager.addThermalStatusListener(this);
+            }
+
+            // If any of the device states are power sensitive, i.e. it should be disabled when
+            // power save mode is enabled, then we will update the list of supported states when
+            // power save mode is toggled.
+            if (hasPowerSaveSensitiveState(deviceStateConfigurations)) {
+                IntentFilter filter = new IntentFilter(
+                        PowerManager.ACTION_POWER_SAVE_MODE_CHANGED_INTERNAL);
+                BroadcastReceiver receiver = new BroadcastReceiver() {
+                    @Override
+                    public void onReceive(Context context, Intent intent) {
+                        if (PowerManager.ACTION_POWER_SAVE_MODE_CHANGED_INTERNAL.equals(
+                                intent.getAction())) {
+                            onPowerSaveModeChanged(powerManager.isPowerSaveMode());
+                        }
+                    }
+                };
+                context.registerReceiver(receiver, filter);
+            }
+        }
+    }
+
+    @Override
+    public void setListener(Listener listener) {
+        synchronized (mLock) {
+            if (mListener != null) {
+                throw new RuntimeException("Provider already has a listener set.");
+            }
+            mListener = listener;
+        }
+        notifySupportedStatesChanged(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED);
+        notifyDeviceStateChangedIfNeeded();
+    }
+
+    /** Notifies the listener that the set of supported device states has changed. */
+    private void notifySupportedStatesChanged(@SupportedStatesUpdatedReason int reason) {
+        List<DeviceState> supportedStates = new ArrayList<>();
+        Listener listener;
+        synchronized (mLock) {
+            if (mListener == null) {
+                return;
+            }
+            listener = mListener;
+            for (DeviceState deviceState : mOrderedStates) {
+                if (isThermalStatusCriticalOrAbove(mThermalStatus)
+                        && deviceState.hasFlag(
+                        DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL)) {
+                    continue;
+                }
+                if (mPowerSaveModeEnabled && deviceState.hasFlag(
+                        DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE)) {
+                    continue;
+                }
+                supportedStates.add(deviceState);
+            }
+        }
+
+        listener.onSupportedDeviceStatesChanged(
+                supportedStates.toArray(new DeviceState[supportedStates.size()]), reason);
+    }
+
+    /** Computes the current device state and notifies the listener of a change, if needed. */
+    void notifyDeviceStateChangedIfNeeded() {
+        int stateToReport = INVALID_DEVICE_STATE;
+        Listener listener;
+        synchronized (mLock) {
+            if (mListener == null) {
+                return;
+            }
+
+            listener = mListener;
+
+            int newState = INVALID_DEVICE_STATE;
+            for (int i = 0; i < mOrderedStates.length; i++) {
+                int state = mOrderedStates[i].getIdentifier();
+                if (DEBUG) {
+                    Slog.d(TAG, "Checking conditions for " + mOrderedStates[i].getName() + "("
+                            + i + ")");
+                }
+                boolean conditionSatisfied;
+                try {
+                    conditionSatisfied = mStateConditions.get(state).getAsBoolean();
+                } catch (IllegalStateException e) {
+                    // Failed to compute the current state based on current available data. Continue
+                    // with the expectation that notifyDeviceStateChangedIfNeeded() will be called
+                    // when a callback with the missing data is triggered. May trigger another state
+                    // change if another state is satisfied currently.
+                    Slog.w(TAG, "Unable to check current state = " + state, e);
+                    dumpSensorValues();
+                    continue;
+                }
+
+                if (conditionSatisfied) {
+                    if (DEBUG) {
+                        Slog.d(TAG, "Device State conditions satisfied, transition to " + state);
+                    }
+                    newState = state;
+                    break;
+                }
+            }
+            if (newState == INVALID_DEVICE_STATE) {
+                Slog.e(TAG, "No declared device states match any of the required conditions.");
+                dumpSensorValues();
+            }
+
+            if (newState != INVALID_DEVICE_STATE && newState != mLastReportedState) {
+                mLastReportedState = newState;
+                stateToReport = newState;
+            }
+        }
+
+        if (stateToReport != INVALID_DEVICE_STATE) {
+            listener.onStateChanged(stateToReport);
+        }
+    }
+
+    @Override
+    public void onSensorChanged(SensorEvent event) {
+        synchronized (mLock) {
+            if (event.sensor == mHallSensor) {
+                mLastHallSensorEvent = event;
+            } else if (event.sensor == mHingeAngleSensor) {
+                mLastHingeAngleSensorEvent = event;
+            }
+        }
+        notifyDeviceStateChangedIfNeeded();
+    }
+
+    @Override
+    public void onAccuracyChanged(Sensor sensor, int accuracy) {
+        // Do nothing.
+    }
+
+    private float getSensorValue(@Nullable SensorEvent sensorEvent) {
+        if (sensorEvent == null) {
+            throw new IllegalStateException("Have not received sensor event.");
+        }
+
+        if (sensorEvent.values.length < 1) {
+            throw new IllegalStateException("Values in the sensor event are empty");
+        }
+
+        return sensorEvent.values[0];
+    }
+
+    @GuardedBy("mLock")
+    private void dumpSensorValues() {
+        Slog.i(TAG, "Sensor values:");
+        dumpSensorValues("Hall Sensor", mHallSensor, mLastHallSensorEvent);
+        dumpSensorValues("Hinge Angle Sensor",mHingeAngleSensor, mLastHingeAngleSensorEvent);
+        Slog.i(TAG, "isScreenOn: " + isScreenOn());
+    }
+
+    @GuardedBy("mLock")
+    private void dumpSensorValues(String sensorType, Sensor sensor, @Nullable SensorEvent event) {
+        String sensorString = sensor == null ? "null" : sensor.getName();
+        String eventValues = event == null ? "null" : Arrays.toString(event.values);
+        Slog.i(TAG, sensorType + " : " + sensorString + " : " + eventValues);
+    }
+
+    @Override
+    public void onDisplayAdded(int displayId) {
+
+    }
+
+    @Override
+    public void onDisplayRemoved(int displayId) {
+
+    }
+
+    @Override
+    public void onDisplayChanged(int displayId) {
+        if (displayId == DEFAULT_DISPLAY) {
+            // Could potentially be moved to the background if needed.
+            try {
+                Trace.beginSection("FoldableDeviceStateProvider#onDisplayChanged()");
+                int displayState = mDisplayManager.getDisplay(displayId).getState();
+                synchronized (mLock) {
+                    mIsScreenOn = displayState == Display.STATE_ON;
+                }
+            } finally {
+                Trace.endSection();
+            }
+        }
+    }
+
+    /**
+     * Configuration for a single device state, contains information about the state like
+     * identifier, name, flags and a predicate that should return true if the state should
+     * be selected.
+     */
+    public static class DeviceStateConfiguration {
+        private final DeviceState mDeviceState;
+        private final Function<FoldableDeviceStateProvider, Boolean> mPredicate;
+
+        private DeviceStateConfiguration(DeviceState deviceState,
+                Function<FoldableDeviceStateProvider, Boolean> predicate) {
+            mDeviceState = deviceState;
+            mPredicate = predicate;
+        }
+
+        public static DeviceStateConfiguration createConfig(
+                @IntRange(from = MINIMUM_DEVICE_STATE, to = MAXIMUM_DEVICE_STATE) int identifier,
+                @NonNull String name,
+                @DeviceState.DeviceStateFlags int flags,
+                Function<FoldableDeviceStateProvider, Boolean> predicate
+        ) {
+            return new DeviceStateConfiguration(new DeviceState(identifier, name, flags),
+                    predicate);
+        }
+
+        public static DeviceStateConfiguration createConfig(
+                @IntRange(from = MINIMUM_DEVICE_STATE, to = MAXIMUM_DEVICE_STATE) int identifier,
+                @NonNull String name,
+                Function<FoldableDeviceStateProvider, Boolean> predicate
+        ) {
+            return new DeviceStateConfiguration(new DeviceState(identifier, name, /* flags= */ 0),
+                    predicate);
+        }
+
+        /**
+         * Creates a device state configuration for a closed tent-mode aware state.
+         *
+         * During tent mode:
+         * - The inner display is OFF
+         * - The outer display is ON
+         * - The device is partially unfolded (left and right edges could be on the table)
+         * In this mode the device the device so it could be used in a posture where both left
+         * and right edges of the unfolded device are on the table.
+         *
+         * The predicate returns false after the hinge angle reaches
+         * {@code tentModeSwitchAngleDegrees}. Then it switches back only when the hinge angle
+         * becomes less than {@code maxClosedAngleDegrees}. Hinge angle is 0 degrees when the device
+         * is fully closed and 180 degrees when it is fully unfolded.
+         *
+         * For example, when tentModeSwitchAngleDegrees = 90 and maxClosedAngleDegrees = 5 degrees:
+         *  - when unfolding the device from fully closed posture (last state == closed or it is
+         *    undefined yet) this state will become not matching after reaching the angle
+         *    of 90 degrees, it allows the device to switch the outer display to the inner display
+         *    only when reaching this threshold
+         *  - when folding (last state != 'closed') this state will become matching after reaching
+         *    the angle less than 5 degrees and when hall sensor detected that the device is closed,
+         *    so the switch from the inner display to the outer will become only when the device
+         *    is fully closed.
+         *
+         * @param identifier state identifier
+         * @param name state name
+         * @param flags state flags
+         * @param minClosedAngleDegrees minimum (inclusive) hinge angle value for the closed state
+         * @param maxClosedAngleDegrees maximum (non-inclusive) hinge angle value for the closed
+         *                              state
+         * @param tentModeSwitchAngleDegrees the angle when this state should switch when unfolding
+         * @return device state configuration
+         */
+        public static DeviceStateConfiguration createTentModeClosedState(
+                @IntRange(from = MINIMUM_DEVICE_STATE, to = MAXIMUM_DEVICE_STATE) int identifier,
+                @NonNull String name,
+                @DeviceState.DeviceStateFlags int flags,
+                int minClosedAngleDegrees,
+                int maxClosedAngleDegrees,
+                int tentModeSwitchAngleDegrees
+        ) {
+            return new DeviceStateConfiguration(new DeviceState(identifier, name, flags),
+                    (stateContext) -> {
+                        final boolean hallSensorClosed = stateContext.isHallSensorClosed();
+                        final float hingeAngle = stateContext.getHingeAngle();
+                        final int lastState = stateContext.getLastReportedDeviceState();
+                        final boolean isScreenOn = stateContext.isScreenOn();
+
+                        final int switchingDegrees =
+                                isScreenOn ? tentModeSwitchAngleDegrees : maxClosedAngleDegrees;
+
+                        final int closedDeviceState = identifier;
+                        final boolean isLastStateClosed = lastState == closedDeviceState
+                                || lastState == INVALID_DEVICE_STATE;
+
+                        final boolean shouldBeClosedBecauseTentMode = isLastStateClosed
+                                && hingeAngle >= minClosedAngleDegrees
+                                && hingeAngle < switchingDegrees;
+
+                        final boolean shouldBeClosedBecauseFullyShut = hallSensorClosed
+                                && hingeAngle >= minClosedAngleDegrees
+                                && hingeAngle < maxClosedAngleDegrees;
+
+                        return shouldBeClosedBecauseFullyShut || shouldBeClosedBecauseTentMode;
+                    });
+        }
+    }
+
+    /**
+     * @return Whether the screen is on.
+     */
+    public boolean isScreenOn() {
+        synchronized (mLock) {
+            return mIsScreenOn;
+        }
+    }
+    /**
+     * @return current hinge angle value of a foldable device
+     */
+    public float getHingeAngle() {
+        synchronized (mLock) {
+            return getSensorValue(mLastHingeAngleSensorEvent);
+        }
+    }
+
+    /**
+     * @return true if hall sensor detected that the device is closed (fully shut)
+     */
+    public boolean isHallSensorClosed() {
+        synchronized (mLock) {
+            return getSensorValue(mLastHallSensorEvent) > 0f;
+        }
+    }
+
+    /**
+     * @return last reported device state
+     */
+    public int getLastReportedDeviceState() {
+        synchronized (mLock) {
+            return mLastReportedState;
+        }
+    }
+
+    @VisibleForTesting
+    void onPowerSaveModeChanged(boolean isPowerSaveModeEnabled) {
+        synchronized (mLock) {
+            if (mPowerSaveModeEnabled != isPowerSaveModeEnabled) {
+                mPowerSaveModeEnabled = isPowerSaveModeEnabled;
+                notifySupportedStatesChanged(
+                        isPowerSaveModeEnabled ? SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_ENABLED
+                                : SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_DISABLED);
+            }
+        }
+    }
+
+    @Override
+    public void onThermalStatusChanged(@PowerManager.ThermalStatus int thermalStatus) {
+        int previousThermalStatus;
+        synchronized (mLock) {
+            previousThermalStatus = mThermalStatus;
+            mThermalStatus = thermalStatus;
+        }
+
+        boolean isThermalStatusCriticalOrAbove = isThermalStatusCriticalOrAbove(thermalStatus);
+        boolean isPreviousThermalStatusCriticalOrAbove =
+                isThermalStatusCriticalOrAbove(previousThermalStatus);
+        if (isThermalStatusCriticalOrAbove != isPreviousThermalStatusCriticalOrAbove) {
+            Slog.i(TAG, "Updating supported device states due to thermal status change."
+                    + " isThermalStatusCriticalOrAbove: " + isThermalStatusCriticalOrAbove);
+            notifySupportedStatesChanged(
+                    isThermalStatusCriticalOrAbove
+                            ? SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_CRITICAL
+                            : SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_NORMAL);
+        }
+    }
+
+    private static boolean isThermalStatusCriticalOrAbove(
+            @PowerManager.ThermalStatus int thermalStatus) {
+        switch (thermalStatus) {
+            case PowerManager.THERMAL_STATUS_CRITICAL:
+            case PowerManager.THERMAL_STATUS_EMERGENCY:
+            case PowerManager.THERMAL_STATUS_SHUTDOWN:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    private static boolean hasThermalSensitiveState(DeviceStateConfiguration[] deviceStates) {
+        for (int i = 0; i < deviceStates.length; i++) {
+            DeviceStateConfiguration state = deviceStates[i];
+            if (state.mDeviceState
+                    .hasFlag(DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean hasPowerSaveSensitiveState(DeviceStateConfiguration[] deviceStates) {
+        for (int i = 0; i < deviceStates.length; i++) {
+            if (deviceStates[i].mDeviceState
+                    .hasFlag(DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/services/foldables/devicestateprovider/tests/Android.bp b/services/foldables/devicestateprovider/tests/Android.bp
new file mode 100644
index 0000000..a8db05e
--- /dev/null
+++ b/services/foldables/devicestateprovider/tests/Android.bp
@@ -0,0 +1,30 @@
+package {
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+    name: "foldable-device-state-provider-tests",
+    srcs: ["src/**/*.java"],
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+        "android.test.mock",
+    ],
+    jni_libs: [
+        "libdexmakerjvmtiagent",
+        "libmultiplejvmtiagentsinterferenceagent",
+        "libstaticjvmtiagent",
+    ],
+    static_libs: [
+        "services",
+        "foldable-device-state-provider",
+        "androidx.test.rules",
+        "junit",
+        "truth-prebuilt",
+        "mockito-target-extended-minus-junit4",
+        "androidx.test.uiautomator_uiautomator",
+        "androidx.test.ext.junit",
+        "testables",
+    ],
+    test_suites: ["device-tests"]
+}
diff --git a/services/foldables/devicestateprovider/tests/AndroidManifest.xml b/services/foldables/devicestateprovider/tests/AndroidManifest.xml
new file mode 100644
index 0000000..736613d
--- /dev/null
+++ b/services/foldables/devicestateprovider/tests/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.foldablesdevicestatelib.tests">
+
+    <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30" />
+
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.foldablesdevicestatelib.tests"
+        android:label="Tests for foldable-device-state-provider library">
+    </instrumentation>
+
+</manifest>
\ No newline at end of file
diff --git a/services/foldables/devicestateprovider/tests/AndroidTest.xml b/services/foldables/devicestateprovider/tests/AndroidTest.xml
new file mode 100644
index 0000000..f5fdac7
--- /dev/null
+++ b/services/foldables/devicestateprovider/tests/AndroidTest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<configuration description="foldable-device-state-provider tests">
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="install-arg" value="-t" />
+        <option name="test-file-name" value="foldable-device-state-provider-tests.apk" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="package" value="com.android.foldablesdevicestatelib.tests" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="hidden-api-checks" value="false" />
+    </test>
+</configuration>
\ No newline at end of file
diff --git a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java
new file mode 100644
index 0000000..8fa4ce5
--- /dev/null
+++ b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java
@@ -0,0 +1,519 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.policy;
+
+
+import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED;
+import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_DISABLED;
+import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_ENABLED;
+import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_CRITICAL;
+import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_NORMAL;
+import com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.STATE_OFF;
+import static android.view.Display.STATE_ON;
+
+import static com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration.createConfig;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.nullable;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorManager;
+import android.hardware.display.DisplayManager;
+import android.hardware.input.InputSensorInfo;
+import android.os.PowerManager;
+import android.os.Handler;
+import android.testing.AndroidTestingRunner;
+import android.view.Display;
+
+import com.android.server.devicestate.DeviceState;
+import com.android.server.devicestate.DeviceStateProvider;
+import com.android.server.devicestate.DeviceStateProvider.Listener;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.internal.util.reflection.FieldSetter;
+
+/**
+ * Unit tests for {@link FoldableDeviceStateProvider}.
+ * <p/>
+ * Run with <code>atest FoldableDeviceStateProviderTest</code>.
+ */
+@RunWith(AndroidTestingRunner.class)
+public final class FoldableDeviceStateProviderTest {
+
+    private final ArgumentCaptor<DeviceState[]> mDeviceStateArrayCaptor = ArgumentCaptor.forClass(
+            DeviceState[].class);
+    @Captor
+    private ArgumentCaptor<Integer> mIntegerCaptor;
+    @Captor
+    private ArgumentCaptor<DisplayManager.DisplayListener> mDisplayListenerCaptor;
+    @Mock
+    private SensorManager mSensorManager;
+    @Mock
+    private Context mContext;
+    @Mock
+    private InputSensorInfo mInputSensorInfo;
+    private Sensor mHallSensor;
+    private Sensor mHingeAngleSensor;
+    @Mock
+    private DisplayManager mDisplayManager;
+    private FoldableDeviceStateProvider mProvider;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+
+        mHallSensor = new Sensor(mInputSensorInfo);
+        mHingeAngleSensor = new Sensor(mInputSensorInfo);
+    }
+
+    @Test
+    public void create_emptyConfiguration_throwsException() {
+        assertThrows(IllegalArgumentException.class, this::createProvider);
+    }
+
+    @Test
+    public void create_duplicatedDeviceStateIdentifiers_throwsException() {
+        assertThrows(IllegalArgumentException.class,
+                () -> createProvider(
+                        createConfig(
+                                /* identifier= */ 0, /* name= */ "ONE", (c) -> true),
+                        createConfig(
+                                /* identifier= */ 0, /* name= */ "TWO", (c) -> true)
+                ));
+    }
+
+    @Test
+    public void create_allMatchingStatesDefaultsToTheFirstIdentifier() {
+        createProvider(
+                createConfig(
+                        /* identifier= */ 1, /* name= */ "ONE", (c) -> true),
+                createConfig(
+                        /* identifier= */ 2, /* name= */ "TWO", (c) -> true),
+                createConfig(
+                        /* identifier= */ 3, /* name= */ "THREE", (c) -> true)
+        );
+
+        Listener listener = mock(Listener.class);
+        mProvider.setListener(listener);
+
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
+        final DeviceState[] expectedStates = new DeviceState[]{
+                new DeviceState(1, "ONE", /* flags= */ 0),
+                new DeviceState(2, "TWO", /* flags= */ 0),
+                new DeviceState(3, "THREE", /* flags= */ 0),
+        };
+        assertArrayEquals(expectedStates, mDeviceStateArrayCaptor.getValue());
+
+        verify(listener).onStateChanged(mIntegerCaptor.capture());
+        assertEquals(1, mIntegerCaptor.getValue().intValue());
+    }
+
+    @Test
+    public void create_multipleMatchingStatesDefaultsToTheLowestIdentifier() {
+        createProvider(
+                createConfig(
+                        /* identifier= */ 1, /* name= */ "ONE", (c) -> false),
+                createConfig(
+                        /* identifier= */ 3, /* name= */ "THREE", (c) -> false),
+                createConfig(
+                        /* identifier= */ 4, /* name= */ "FOUR", (c) -> true),
+                createConfig(
+                        /* identifier= */ 2, /* name= */ "TWO", (c) -> true)
+        );
+
+        Listener listener = mock(Listener.class);
+        mProvider.setListener(listener);
+
+        verify(listener).onStateChanged(mIntegerCaptor.capture());
+        assertEquals(2, mIntegerCaptor.getValue().intValue());
+    }
+
+    @Test
+    public void test_hingeAngleUpdatedFirstTime_switchesToMatchingState() throws Exception {
+        createProvider(createConfig(/* identifier= */ 1, /* name= */ "ONE",
+                        (c) -> c.getHingeAngle() < 90f),
+                createConfig(/* identifier= */ 2, /* name= */ "TWO",
+                        (c) -> c.getHingeAngle() >= 90f));
+        Listener listener = mock(Listener.class);
+        mProvider.setListener(listener);
+        verify(listener, never()).onStateChanged(anyInt());
+        clearInvocations(listener);
+
+        sendSensorEvent(mHingeAngleSensor, /* value= */ 100f);
+
+        verify(listener).onStateChanged(mIntegerCaptor.capture());
+        assertEquals(2, mIntegerCaptor.getValue().intValue());
+    }
+
+    @Test
+    public void test_hallSensorUpdatedFirstTime_switchesToMatchingState() throws Exception {
+        createProvider(createConfig(/* identifier= */ 1, /* name= */ "ONE",
+                        (c) -> !c.isHallSensorClosed()),
+                createConfig(/* identifier= */ 2, /* name= */ "TWO",
+                        FoldableDeviceStateProvider::isHallSensorClosed));
+        Listener listener = mock(Listener.class);
+        mProvider.setListener(listener);
+        verify(listener, never()).onStateChanged(anyInt());
+        clearInvocations(listener);
+
+        // Hall sensor value '1f' is for the closed state
+        sendSensorEvent(mHallSensor, /* value= */ 1f);
+
+        verify(listener).onStateChanged(mIntegerCaptor.capture());
+        assertEquals(2, mIntegerCaptor.getValue().intValue());
+    }
+
+    @Test
+    public void test_hingeAngleUpdatedSecondTime_switchesToMatchingState() throws Exception {
+        createProvider(createConfig(/* identifier= */ 1, /* name= */ "ONE",
+                        (c) -> c.getHingeAngle() < 90f),
+                createConfig(/* identifier= */ 2, /* name= */ "TWO",
+                        (c) -> c.getHingeAngle() >= 90f));
+        sendSensorEvent(mHingeAngleSensor, /* value= */ 30f);
+        Listener listener = mock(Listener.class);
+        mProvider.setListener(listener);
+        verify(listener).onStateChanged(mIntegerCaptor.capture());
+        assertEquals(1, mIntegerCaptor.getValue().intValue());
+        clearInvocations(listener);
+
+        sendSensorEvent(mHingeAngleSensor, /* value= */ 100f);
+
+        verify(listener).onStateChanged(mIntegerCaptor.capture());
+        assertEquals(2, mIntegerCaptor.getValue().intValue());
+    }
+
+    @Test
+    public void test_hallSensorUpdatedSecondTime_switchesToMatchingState() throws Exception {
+        createProvider(createConfig(/* identifier= */ 1, /* name= */ "ONE",
+                        (c) -> !c.isHallSensorClosed()),
+                createConfig(/* identifier= */ 2, /* name= */ "TWO",
+                        FoldableDeviceStateProvider::isHallSensorClosed));
+        sendSensorEvent(mHallSensor, /* value= */ 0f);
+        Listener listener = mock(Listener.class);
+        mProvider.setListener(listener);
+        verify(listener).onStateChanged(mIntegerCaptor.capture());
+        assertEquals(1, mIntegerCaptor.getValue().intValue());
+        clearInvocations(listener);
+
+        // Hall sensor value '1f' is for the closed state
+        sendSensorEvent(mHallSensor, /* value= */ 1f);
+
+        verify(listener).onStateChanged(mIntegerCaptor.capture());
+        assertEquals(2, mIntegerCaptor.getValue().intValue());
+    }
+
+    @Test
+    public void test_invalidSensorValues_onStateChangedIsNotTriggered() throws Exception {
+        createProvider(createConfig(/* identifier= */ 1, /* name= */ "ONE",
+                        (c) -> c.getHingeAngle() < 90f),
+                createConfig(/* identifier= */ 2, /* name= */ "TWO",
+                        (c) -> c.getHingeAngle() >= 90f));
+        Listener listener = mock(Listener.class);
+        mProvider.setListener(listener);
+        clearInvocations(listener);
+
+        // First, switch to a non-default state.
+        sendSensorEvent(mHingeAngleSensor, /* value= */ 100f);
+        verify(listener).onStateChanged(mIntegerCaptor.capture());
+        assertEquals(2, mIntegerCaptor.getValue().intValue());
+
+        clearInvocations(listener);
+
+        // Then, send an invalid sensor event, verify that onStateChanged() is not triggered.
+        sendInvalidSensorEvent(mHingeAngleSensor);
+
+        verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
+        verify(listener, never()).onStateChanged(mIntegerCaptor.capture());
+    }
+
+    @Test
+    public void test_nullSensorValues_noExceptionThrown() throws Exception {
+        createProvider(createConfig( /* identifier= */ 1, /* name= */ "ONE",
+                        (c) -> c.getHingeAngle() < 90f));
+        sendInvalidSensorEvent(null);
+    }
+
+    @Test
+    public void test_flagDisableWhenThermalStatusCritical() throws Exception {
+        createProvider(createConfig(/* identifier= */ 1, /* name= */ "CLOSED",
+                        (c) -> c.getHingeAngle() < 5f),
+                createConfig(/* identifier= */ 2, /* name= */ "HALF_OPENED",
+                        (c) -> c.getHingeAngle() < 90f),
+                createConfig(/* identifier= */ 3, /* name= */ "OPENED",
+                        (c) -> c.getHingeAngle() < 180f),
+                createConfig(/* identifier= */ 4, /* name= */ "THERMAL_TEST",
+                        DeviceState.FLAG_EMULATED_ONLY
+                                | DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL
+                                | DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE,
+                        (c) -> true));
+        Listener listener = mock(Listener.class);
+        mProvider.setListener(listener);
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
+        assertArrayEquals(
+                new DeviceState[]{
+                        new DeviceState(1, "CLOSED", 0 /* flags */),
+                        new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+                        new DeviceState(3, "OPENED", 0 /* flags */),
+                        new DeviceState(4, "THERMAL_TEST",
+                                DeviceState.FLAG_EMULATED_ONLY
+                                        | DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL
+                                        | DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE)},
+                mDeviceStateArrayCaptor.getValue());
+        clearInvocations(listener);
+
+        mProvider.onThermalStatusChanged(PowerManager.THERMAL_STATUS_MODERATE);
+        verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
+        clearInvocations(listener);
+
+        // The THERMAL_TEST state should be disabled.
+        mProvider.onThermalStatusChanged(PowerManager.THERMAL_STATUS_CRITICAL);
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_CRITICAL));
+        assertArrayEquals(
+                new DeviceState[]{
+                        new DeviceState(1, "CLOSED", 0 /* flags */),
+                        new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+                        new DeviceState(3, "OPENED", 0 /* flags */)},
+                mDeviceStateArrayCaptor.getValue());
+        clearInvocations(listener);
+
+        // The THERMAL_TEST state should be re-enabled.
+        mProvider.onThermalStatusChanged(PowerManager.THERMAL_STATUS_LIGHT);
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_NORMAL));
+        assertArrayEquals(
+                new DeviceState[]{
+                        new DeviceState(1, "CLOSED", 0 /* flags */),
+                        new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+                        new DeviceState(3, "OPENED", 0 /* flags */),
+                        new DeviceState(4, "THERMAL_TEST",
+                                DeviceState.FLAG_EMULATED_ONLY
+                                        | DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL
+                                        | DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE)},
+                mDeviceStateArrayCaptor.getValue());
+    }
+
+    @Test
+    public void test_flagDisableWhenPowerSaveEnabled() throws Exception {
+        createProvider(createConfig(/* identifier= */ 1, /* name= */ "CLOSED",
+                        (c) -> c.getHingeAngle() < 5f),
+                createConfig(/* identifier= */ 2, /* name= */ "HALF_OPENED",
+                        (c) -> c.getHingeAngle() < 90f),
+                createConfig(/* identifier= */ 3, /* name= */ "OPENED",
+                        (c) -> c.getHingeAngle() < 180f),
+                createConfig(/* identifier= */ 4, /* name= */ "THERMAL_TEST",
+                        DeviceState.FLAG_EMULATED_ONLY
+                                | DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL
+                                | DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE,
+                        (c) -> true));
+        mProvider.onPowerSaveModeChanged(false /* isPowerSaveModeEnabled */);
+        Listener listener = mock(Listener.class);
+        mProvider.setListener(listener);
+
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
+        assertArrayEquals(
+                new DeviceState[]{
+                        new DeviceState(1, "CLOSED", 0 /* flags */),
+                        new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+                        new DeviceState(3, "OPENED", 0 /* flags */),
+                        new DeviceState(4, "THERMAL_TEST",
+                                DeviceState.FLAG_EMULATED_ONLY
+                                        | DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL
+                                        | DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE) },
+                mDeviceStateArrayCaptor.getValue());
+        clearInvocations(listener);
+
+        mProvider.onPowerSaveModeChanged(false /* isPowerSaveModeEnabled */);
+        verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
+        clearInvocations(listener);
+
+        // The THERMAL_TEST state should be disabled due to power save being enabled.
+        mProvider.onPowerSaveModeChanged(true /* isPowerSaveModeEnabled */);
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_ENABLED));
+        assertArrayEquals(
+                new DeviceState[]{
+                        new DeviceState(1, "CLOSED", 0 /* flags */),
+                        new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+                        new DeviceState(3, "OPENED", 0 /* flags */) },
+                mDeviceStateArrayCaptor.getValue());
+        clearInvocations(listener);
+
+        // The THERMAL_TEST state should be re-enabled.
+        mProvider.onPowerSaveModeChanged(false /* isPowerSaveModeEnabled */);
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_DISABLED));
+        assertArrayEquals(
+                new DeviceState[]{
+                        new DeviceState(1, "CLOSED", 0 /* flags */),
+                        new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+                        new DeviceState(3, "OPENED", 0 /* flags */),
+                        new DeviceState(4, "THERMAL_TEST",
+                                DeviceState.FLAG_EMULATED_ONLY
+                                        | DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL
+                                        | DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE) },
+                mDeviceStateArrayCaptor.getValue());
+    }
+
+    @Test
+    public void test_previousStateBasedPredicate() {
+        // Create a configuration where state TWO could be matched only if
+        // the previous state was 'THREE'
+        createProvider(
+                createConfig(
+                        /* identifier= */ 1, /* name= */ "ONE", (c) -> c.getHingeAngle() < 30f),
+                createConfig(
+                        /* identifier= */ 2, /* name= */ "TWO",
+                        (c) -> c.getLastReportedDeviceState() == 3 && c.getHingeAngle() > 120f),
+                createConfig(
+                        /* identifier= */ 3, /* name= */ "THREE",
+                        (c) -> c.getHingeAngle() > 90f)
+        );
+        sendSensorEvent(mHingeAngleSensor, /* value= */ 0f);
+        Listener listener = mock(Listener.class);
+        mProvider.setListener(listener);
+
+        // Check that the initial state is 'ONE'
+        verify(listener).onStateChanged(mIntegerCaptor.capture());
+        assertEquals(1, mIntegerCaptor.getValue().intValue());
+        clearInvocations(listener);
+
+        // Should not match state 'TWO', it should match only state 'THREE'
+        // (because the previous state is not 'THREE', it is 'ONE')
+        sendSensorEvent(mHingeAngleSensor, /* value= */ 180f);
+        verify(listener).onStateChanged(mIntegerCaptor.capture());
+        assertEquals(3, mIntegerCaptor.getValue().intValue());
+        clearInvocations(listener);
+
+        // Now it should match state 'TWO'
+        // (because the previous state is 'THREE' now)
+        sendSensorEvent(mHingeAngleSensor, /* value= */ 180f);
+        verify(listener).onStateChanged(mIntegerCaptor.capture());
+        assertEquals(2, mIntegerCaptor.getValue().intValue());
+    }
+
+    @Test
+    public void isScreenOn_afterDisplayChangedToOn_returnsTrue() {
+        createProvider(
+                createConfig(
+                        /* identifier= */ 1, /* name= */ "ONE", (c) -> true)
+        );
+
+        setScreenOn(true);
+
+        assertThat(mProvider.isScreenOn()).isTrue();
+    }
+
+    @Test
+    public void isScreenOn_afterDisplayChangedToOff_returnsFalse() {
+        createProvider(
+                createConfig(
+                        /* identifier= */ 1, /* name= */ "ONE", (c) -> true)
+        );
+
+        setScreenOn(false);
+
+        assertThat(mProvider.isScreenOn()).isFalse();
+    }
+
+    @Test
+    public void isScreenOn_afterDisplayChangedToOnThenOff_returnsFalse() {
+        createProvider(
+                createConfig(
+                        /* identifier= */ 1, /* name= */ "ONE", (c) -> true)
+        );
+
+        setScreenOn(true);
+        setScreenOn(false);
+
+        assertThat(mProvider.isScreenOn()).isFalse();
+    }
+
+    private void setScreenOn(boolean isOn) {
+        Display mockDisplay = mock(Display.class);
+        int state = isOn ? STATE_ON : STATE_OFF;
+        when(mockDisplay.getState()).thenReturn(state);
+        when(mDisplayManager.getDisplay(eq(DEFAULT_DISPLAY))).thenReturn(mockDisplay);
+        mDisplayListenerCaptor.getValue().onDisplayChanged(DEFAULT_DISPLAY);
+    }
+
+    private void sendSensorEvent(Sensor sensor, float value) {
+        SensorEvent event = mock(SensorEvent.class);
+        event.sensor = sensor;
+        try {
+            FieldSetter.setField(event, event.getClass().getField("values"),
+                    new float[]{value});
+        } catch (NoSuchFieldException e) {
+            e.printStackTrace();
+        }
+
+        mProvider.onSensorChanged(event);
+    }
+
+    private void sendInvalidSensorEvent(Sensor sensor) {
+        SensorEvent event = mock(SensorEvent.class);
+        event.sensor = sensor;
+        try {
+            // Set empty values array to make the event invalid
+            FieldSetter.setField(event, event.getClass().getField("values"),
+                    new float[]{});
+        } catch (NoSuchFieldException e) {
+            e.printStackTrace();
+        }
+        mProvider.onSensorChanged(event);
+    }
+
+    private void createProvider(DeviceStateConfiguration... configurations) {
+        mProvider = new FoldableDeviceStateProvider(mContext, mSensorManager, mHingeAngleSensor,
+                mHallSensor, mDisplayManager, configurations);
+        verify(mDisplayManager)
+                .registerDisplayListener(
+                        mDisplayListenerCaptor.capture(),
+                        nullable(Handler.class),
+                        anyLong());
+    }
+}
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/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionDefinitionsTest.kt b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionDefinitionsTest.kt
new file mode 100644
index 0000000..832136c
--- /dev/null
+++ b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionDefinitionsTest.kt
@@ -0,0 +1,764 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.permission.test
+
+import android.content.pm.PermissionInfo
+import android.os.Build
+import com.android.server.permission.access.MutateStateScope
+import com.android.server.permission.access.permission.Permission
+import com.android.server.permission.access.permission.PermissionFlags
+import com.android.server.pm.pkg.PackageState
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * Parameterized test for testing permission definitions (adopt permissions, add permission groups,
+ * add permissions, trim permissions, trim permission states and revoke permissions on
+ * package update) for onStorageVolumeAdded() and onPackageAdded() in AppIdPermissionPolicy.
+ *
+ * Note that the evaluatePermissionState() call with permission changes
+ * (i.e. changedPermissionNames in AppIdPermissionPolicy) and the evaluatePermissionState() call
+ * with an installedPackageState is put in this test instead of
+ * AppIdPermissionPolicyPermissionStatesTest because these concepts don't apply to onUserAdded().
+ */
+@RunWith(Parameterized::class)
+class AppIdPermissionPolicyPermissionDefinitionsTest : BaseAppIdPermissionPolicyTest() {
+    @Parameterized.Parameter(0) lateinit var action: Action
+
+    @Test
+    fun testAdoptPermissions_permissionsOfMissingSystemApp_getsAdopted() {
+        testAdoptPermissions(hasMissingPackage = true, isSystem = true)
+
+        assertWithMessage(
+            "After $action is called for a null adopt permission package," +
+                " the permission package name: ${getPermission(PERMISSION_NAME_0)?.packageName}" +
+                " did not match the expected package name: $PACKAGE_NAME_0"
+        )
+            .that(getPermission(PERMISSION_NAME_0)?.packageName)
+            .isEqualTo(PACKAGE_NAME_0)
+    }
+
+    @Test
+    fun testAdoptPermissions_permissionsOfExistingSystemApp_notAdopted() {
+        testAdoptPermissions(isSystem = true)
+
+        assertWithMessage(
+            "After $action is called for a non-null adopt permission" +
+                " package, the permission package name:" +
+                " ${getPermission(PERMISSION_NAME_0)?.packageName} should not match the" +
+                " package name: $PACKAGE_NAME_0"
+        )
+            .that(getPermission(PERMISSION_NAME_0)?.packageName)
+            .isNotEqualTo(PACKAGE_NAME_0)
+    }
+
+    @Test
+    fun testAdoptPermissions_permissionsOfNonSystemApp_notAdopted() {
+        testAdoptPermissions(hasMissingPackage = true)
+
+        assertWithMessage(
+            "After $action is called for a non-system adopt permission" +
+                " package, the permission package name:" +
+                " ${getPermission(PERMISSION_NAME_0)?.packageName} should not match the" +
+                " package name: $PACKAGE_NAME_0"
+        )
+            .that(getPermission(PERMISSION_NAME_0)?.packageName)
+            .isNotEqualTo(PACKAGE_NAME_0)
+    }
+
+    private fun testAdoptPermissions(
+        hasMissingPackage: Boolean = false,
+        isSystem: Boolean = false
+    ) {
+        val parsedPermission = mockParsedPermission(PERMISSION_NAME_0, PACKAGE_NAME_1)
+        val packageToAdoptPermission = if (hasMissingPackage) {
+            mockPackageState(APP_ID_1, PACKAGE_NAME_1, isSystem = isSystem)
+        } else {
+            mockPackageState(
+                APP_ID_1,
+                mockAndroidPackage(
+                    PACKAGE_NAME_1,
+                    permissions = listOf(parsedPermission)
+                ),
+                isSystem = isSystem
+            )
+        }
+        addPackageState(packageToAdoptPermission)
+        addPermission(parsedPermission)
+
+        mutateState {
+            val installedPackage = mockPackageState(
+                APP_ID_0,
+                mockAndroidPackage(
+                    PACKAGE_NAME_0,
+                    permissions = listOf(defaultPermission),
+                    adoptPermissions = listOf(PACKAGE_NAME_1)
+                )
+            )
+            addPackageState(installedPackage, newState)
+            testAction(installedPackage)
+        }
+    }
+
+    @Test
+    fun testPermissionGroupDefinition_newPermissionGroup_getsDeclared() {
+        mutateState {
+            val packageState = mockPackageState(APP_ID_0, mockSimpleAndroidPackage())
+            addPackageState(packageState, newState)
+            testAction(packageState)
+        }
+
+        assertWithMessage(
+            "After $action is called when there is no existing" +
+                " permission groups, the new permission group $PERMISSION_GROUP_NAME_0 is not added"
+        )
+            .that(getPermissionGroup(PERMISSION_GROUP_NAME_0)?.name)
+            .isEqualTo(PERMISSION_GROUP_NAME_0)
+    }
+
+    @Test
+    fun testPermissionGroupDefinition_systemAppTakingOverPermissionGroupDefinition_getsTakenOver() {
+        testTakingOverPermissionAndPermissionGroupDefinitions(newPermissionOwnerIsSystem = true)
+
+        assertWithMessage(
+            "After $action is called when $PERMISSION_GROUP_NAME_0 already" +
+                " exists in the system, the system app $PACKAGE_NAME_0 didn't takeover the" +
+                " ownership of this permission group"
+        )
+            .that(getPermissionGroup(PERMISSION_GROUP_NAME_0)?.packageName)
+            .isEqualTo(PACKAGE_NAME_0)
+    }
+
+    @Test
+    fun testPermissionGroupDefinition_instantApps_remainsUnchanged() {
+        testTakingOverPermissionAndPermissionGroupDefinitions(
+            newPermissionOwnerIsInstant = true,
+            permissionGroupAlreadyExists = false
+        )
+
+        assertWithMessage(
+            "After $action is called for an instant app," +
+                " the new permission group $PERMISSION_GROUP_NAME_0 should not be added"
+        )
+            .that(getPermissionGroup(PERMISSION_GROUP_NAME_0))
+            .isNull()
+    }
+
+    @Test
+    fun testPermissionGroupDefinition_nonSystemAppTakingOverGroupDefinition_remainsUnchanged() {
+        testTakingOverPermissionAndPermissionGroupDefinitions()
+
+        assertWithMessage(
+            "After $action is called when $PERMISSION_GROUP_NAME_0 already" +
+                " exists in the system, non-system app $PACKAGE_NAME_0 shouldn't takeover" +
+                " ownership of this permission group"
+        )
+            .that(getPermissionGroup(PERMISSION_GROUP_NAME_0)?.packageName)
+            .isEqualTo(PACKAGE_NAME_1)
+    }
+
+    @Test
+    fun testPermissionGroupDefinition_takingOverGroupDeclaredBySystemApp_remainsUnchanged() {
+        testTakingOverPermissionAndPermissionGroupDefinitions(oldPermissionOwnerIsSystem = true)
+
+        assertWithMessage(
+            "After $action is called when $PERMISSION_GROUP_NAME_0 already" +
+                " exists in the system and is owned by a system app, app $PACKAGE_NAME_0" +
+                " shouldn't takeover ownership of this permission group"
+        )
+            .that(getPermissionGroup(PERMISSION_GROUP_NAME_0)?.packageName)
+            .isEqualTo(PACKAGE_NAME_1)
+    }
+
+    @Test
+    fun testPermissionDefinition_newPermission_getsDeclared() {
+        mutateState {
+            val packageState = mockPackageState(APP_ID_0, mockSimpleAndroidPackage())
+            addPackageState(packageState, newState)
+            testAction(packageState)
+        }
+
+        assertWithMessage(
+            "After $action is called when there is no existing" +
+                " permissions, the new permission $PERMISSION_NAME_0 is not added"
+        )
+            .that(getPermission(PERMISSION_NAME_0)?.name)
+            .isEqualTo(PERMISSION_NAME_0)
+    }
+
+    @Test
+    fun testPermissionDefinition_configPermission_getsTakenOver() {
+        testTakingOverPermissionAndPermissionGroupDefinitions(
+            oldPermissionOwnerIsSystem = true,
+            newPermissionOwnerIsSystem = true,
+            type = Permission.TYPE_CONFIG,
+            isReconciled = false
+        )
+
+        assertWithMessage(
+            "After $action is called for a config permission with" +
+                " no owner, the ownership is not taken over by a system app $PACKAGE_NAME_0"
+        )
+            .that(getPermission(PERMISSION_NAME_0)?.packageName)
+            .isEqualTo(PACKAGE_NAME_0)
+    }
+
+    @Test
+    fun testPermissionDefinition_systemAppTakingOverPermissionDefinition_getsTakenOver() {
+        testTakingOverPermissionAndPermissionGroupDefinitions(newPermissionOwnerIsSystem = true)
+
+        assertWithMessage(
+            "After $action is called when $PERMISSION_NAME_0 already" +
+                " exists in the system, the system app $PACKAGE_NAME_0 didn't takeover ownership" +
+                " of this permission"
+        )
+            .that(getPermission(PERMISSION_NAME_0)?.packageName)
+            .isEqualTo(PACKAGE_NAME_0)
+    }
+
+    @Test
+    fun testPermissionDefinition_nonSystemAppTakingOverPermissionDefinition_remainsUnchanged() {
+        testTakingOverPermissionAndPermissionGroupDefinitions()
+
+        assertWithMessage(
+            "After $action is called when $PERMISSION_NAME_0 already" +
+                " exists in the system, the non-system app $PACKAGE_NAME_0 shouldn't takeover" +
+                " ownership of this permission"
+        )
+            .that(getPermission(PERMISSION_NAME_0)?.packageName)
+            .isEqualTo(PACKAGE_NAME_1)
+    }
+
+    @Test
+    fun testPermissionDefinition_takingOverPermissionDeclaredBySystemApp_remainsUnchanged() {
+        testTakingOverPermissionAndPermissionGroupDefinitions(oldPermissionOwnerIsSystem = true)
+
+        assertWithMessage(
+            "After $action is called when $PERMISSION_NAME_0 already" +
+                " exists in system and is owned by a system app, the $PACKAGE_NAME_0 shouldn't" +
+                " takeover ownership of this permission"
+        )
+            .that(getPermission(PERMISSION_NAME_0)?.packageName)
+            .isEqualTo(PACKAGE_NAME_1)
+    }
+
+    private fun testTakingOverPermissionAndPermissionGroupDefinitions(
+        oldPermissionOwnerIsSystem: Boolean = false,
+        newPermissionOwnerIsSystem: Boolean = false,
+        newPermissionOwnerIsInstant: Boolean = false,
+        permissionGroupAlreadyExists: Boolean = true,
+        permissionAlreadyExists: Boolean = true,
+        type: Int = Permission.TYPE_MANIFEST,
+        isReconciled: Boolean = true,
+    ) {
+        val oldPermissionOwnerPackageState = mockPackageState(
+            APP_ID_1,
+            PACKAGE_NAME_1,
+            isSystem = oldPermissionOwnerIsSystem
+        )
+        addPackageState(oldPermissionOwnerPackageState)
+        if (permissionGroupAlreadyExists) {
+            addPermissionGroup(mockParsedPermissionGroup(PERMISSION_GROUP_NAME_0, PACKAGE_NAME_1))
+        }
+        if (permissionAlreadyExists) {
+            addPermission(
+                mockParsedPermission(PERMISSION_NAME_0, PACKAGE_NAME_1),
+                type = type,
+                isReconciled = isReconciled
+            )
+        }
+
+        mutateState {
+            val newPermissionOwnerPackageState = mockPackageState(
+                APP_ID_0,
+                mockSimpleAndroidPackage(),
+                isSystem = newPermissionOwnerIsSystem,
+                isInstantApp = newPermissionOwnerIsInstant
+            )
+            addPackageState(newPermissionOwnerPackageState, newState)
+            testAction(newPermissionOwnerPackageState)
+        }
+    }
+
+    @Test
+    fun testPermissionChanged_permissionGroupChanged_getsRevoked() {
+        testPermissionChanged(
+            oldPermissionGroup = PERMISSION_GROUP_NAME_1,
+            newPermissionGroup = PERMISSION_GROUP_NAME_0
+        )
+
+        val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0)
+        val expectedNewFlags = 0
+        assertWithMessage(
+            "After $action is called for a package that has a permission group change" +
+                " for a permission it defines, the actual permission flags $actualFlags" +
+                " should match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testPermissionChanged_protectionLevelChanged_getsRevoked() {
+        testPermissionChanged(newProtectionLevel = PermissionInfo.PROTECTION_INTERNAL)
+
+        val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0)
+        val expectedNewFlags = 0
+        assertWithMessage(
+            "After $action is called for a package that has a protection level change" +
+                " for a permission it defines, the actual permission flags $actualFlags" +
+                " should match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    private fun testPermissionChanged(
+        oldPermissionGroup: String? = null,
+        newPermissionGroup: String? = null,
+        newProtectionLevel: Int = PermissionInfo.PROTECTION_DANGEROUS
+    ) {
+        val oldPermission = mockParsedPermission(
+            PERMISSION_NAME_0,
+            PACKAGE_NAME_0,
+            group = oldPermissionGroup,
+            protectionLevel = PermissionInfo.PROTECTION_DANGEROUS
+        )
+        val oldPackageState = mockPackageState(
+            APP_ID_0,
+            mockAndroidPackage(PACKAGE_NAME_0, permissions = listOf(oldPermission))
+        )
+        addPackageState(oldPackageState)
+        addPermission(oldPermission)
+        setPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0, PermissionFlags.RUNTIME_GRANTED)
+
+        mutateState {
+            val newPermission = mockParsedPermission(
+                PERMISSION_NAME_0,
+                PACKAGE_NAME_0,
+                group = newPermissionGroup,
+                protectionLevel = newProtectionLevel
+            )
+            val newPackageState = mockPackageState(
+                APP_ID_0,
+                mockAndroidPackage(PACKAGE_NAME_0, permissions = listOf(newPermission))
+            )
+            addPackageState(newPackageState, newState)
+            testAction(newPackageState)
+        }
+    }
+
+    @Test
+    fun testPermissionDeclaration_permissionTreeNoLongerDeclared_getsDefinitionRemoved() {
+        testPermissionDeclaration {}
+
+        assertWithMessage(
+            "After $action is called for a package that no longer defines a permission" +
+                " tree, the permission tree: $PERMISSION_NAME_0 in system state should be removed"
+        )
+            .that(getPermissionTree(PERMISSION_NAME_0))
+            .isNull()
+    }
+
+    @Test
+    fun testPermissionDeclaration_permissionTreeByDisabledSystemPackage_remainsUnchanged() {
+        testPermissionDeclaration {
+            val disabledSystemPackageState = mockPackageState(APP_ID_0, mockSimpleAndroidPackage())
+            addDisabledSystemPackageState(disabledSystemPackageState)
+        }
+
+        assertWithMessage(
+            "After $action is called for a package that no longer defines" +
+                " a permission tree while this permission tree is still defined by" +
+                " a disabled system package, the permission tree: $PERMISSION_NAME_0 in" +
+                " system state should not be removed"
+        )
+            .that(getPermissionTree(PERMISSION_TREE_NAME))
+            .isNotNull()
+    }
+
+    @Test
+    fun testPermissionDeclaration_permissionNoLongerDeclared_getsDefinitionRemoved() {
+        testPermissionDeclaration {}
+
+        assertWithMessage(
+            "After $action is called for a package that no longer defines a permission," +
+                " the permission: $PERMISSION_NAME_0 in system state should be removed"
+        )
+            .that(getPermission(PERMISSION_NAME_0))
+            .isNull()
+    }
+
+    @Test
+    fun testPermissionDeclaration_permissionByDisabledSystemPackage_remainsUnchanged() {
+        testPermissionDeclaration {
+            val disabledSystemPackageState = mockPackageState(APP_ID_0, mockSimpleAndroidPackage())
+            addDisabledSystemPackageState(disabledSystemPackageState)
+        }
+
+        assertWithMessage(
+            "After $action is called for a disabled system package and it's updated apk" +
+                " no longer defines a permission, the permission: $PERMISSION_NAME_0 in" +
+                " system state should not be removed"
+        )
+            .that(getPermission(PERMISSION_NAME_0))
+            .isNotNull()
+    }
+
+    private fun testPermissionDeclaration(additionalSetup: () -> Unit) {
+        val oldPackageState = mockPackageState(APP_ID_0, mockSimpleAndroidPackage())
+        addPackageState(oldPackageState)
+        addPermission(defaultPermissionTree)
+        addPermission(defaultPermission)
+
+        additionalSetup()
+
+        mutateState {
+            val newPackageState = mockPackageState(APP_ID_0, mockAndroidPackage(PACKAGE_NAME_0))
+            addPackageState(newPackageState, newState)
+            testAction(newPackageState)
+        }
+    }
+
+    @Test
+    fun testTrimPermissionStates_permissionsNoLongerRequested_getsFlagsRevoked() {
+        val parsedPermission = mockParsedPermission(
+            PERMISSION_NAME_0,
+            PACKAGE_NAME_0,
+            protectionLevel = PermissionInfo.PROTECTION_DANGEROUS
+        )
+        val oldPackageState = mockPackageState(
+            APP_ID_0,
+            mockAndroidPackage(
+                PACKAGE_NAME_0,
+                permissions = listOf(parsedPermission),
+                requestedPermissions = setOf(PERMISSION_NAME_0)
+            )
+        )
+        addPackageState(oldPackageState)
+        addPermission(parsedPermission)
+        setPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0, PermissionFlags.RUNTIME_GRANTED)
+
+        mutateState {
+            val newPackageState = mockPackageState(APP_ID_0, mockSimpleAndroidPackage())
+            addPackageState(newPackageState, newState)
+            testAction(newPackageState)
+        }
+
+        val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0)
+        val expectedNewFlags = 0
+        assertWithMessage(
+            "After $action is called for a package that no longer requests a permission" +
+                " the actual permission flags $actualFlags should match the" +
+                " expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testRevokePermissionsOnPackageUpdate_storageAndMediaDowngradingPastQ_getsRuntimeRevoked() {
+        testRevokePermissionsOnPackageUpdate(
+            PermissionFlags.RUNTIME_GRANTED,
+            newTargetSdkVersion = Build.VERSION_CODES.P
+        )
+
+        val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_READ_EXTERNAL_STORAGE)
+        val expectedNewFlags = 0
+        assertWithMessage(
+            "After $action is called for a package that's downgrading past Q" +
+                " the actual permission flags $actualFlags should match the" +
+                " expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testRevokePermissionsOnPackageUpdate_storageAndMediaNotDowngradingPastQ_remainsUnchanged() {
+        val oldFlags = PermissionFlags.RUNTIME_GRANTED
+        testRevokePermissionsOnPackageUpdate(
+            oldFlags,
+            oldTargetSdkVersion = Build.VERSION_CODES.P,
+            newTargetSdkVersion = Build.VERSION_CODES.P
+        )
+
+        val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_READ_EXTERNAL_STORAGE)
+        val expectedNewFlags = oldFlags
+        assertWithMessage(
+            "After $action is called for a package that's not downgrading past Q" +
+                " the actual permission flags $actualFlags should match the" +
+                " expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testRevokePermissionsOnPackageUpdate_policyFixedDowngradingPastQ_remainsUnchanged() {
+        val oldFlags = PermissionFlags.RUNTIME_GRANTED and PermissionFlags.POLICY_FIXED
+        testRevokePermissionsOnPackageUpdate(oldFlags, newTargetSdkVersion = Build.VERSION_CODES.P)
+
+        val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_READ_EXTERNAL_STORAGE)
+        val expectedNewFlags = oldFlags
+        assertWithMessage(
+            "After $action is called for a package that's downgrading past Q" +
+                " the actual permission flags with PermissionFlags.POLICY_FIXED $actualFlags" +
+                " should match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testRevokePermissionsOnPackageUpdate_newlyRequestingLegacyExternalStorage_runtimeRevoked() {
+        testRevokePermissionsOnPackageUpdate(
+            PermissionFlags.RUNTIME_GRANTED,
+            oldTargetSdkVersion = Build.VERSION_CODES.P,
+            newTargetSdkVersion = Build.VERSION_CODES.P,
+            oldIsRequestLegacyExternalStorage = false
+        )
+
+        val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_READ_EXTERNAL_STORAGE)
+        val expectedNewFlags = 0
+        assertWithMessage(
+            "After $action is called for a package with" +
+                " newlyRequestingLegacyExternalStorage, the actual permission flags $actualFlags" +
+                " should match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testRevokePermissionsOnPackageUpdate_missingOldPackage_remainsUnchanged() {
+        val oldFlags = PermissionFlags.RUNTIME_GRANTED
+        testRevokePermissionsOnPackageUpdate(
+            oldFlags,
+            newTargetSdkVersion = Build.VERSION_CODES.P,
+            isOldPackageMissing = true
+        )
+
+        val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_READ_EXTERNAL_STORAGE)
+        val expectedNewFlags = oldFlags
+        assertWithMessage(
+            "After $action is called for a package that's downgrading past Q" +
+                " and doesn't have the oldPackage, the actual permission flags $actualFlags" +
+                " should match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    private fun testRevokePermissionsOnPackageUpdate(
+        oldFlags: Int,
+        oldTargetSdkVersion: Int = Build.VERSION_CODES.UPSIDE_DOWN_CAKE,
+        newTargetSdkVersion: Int = Build.VERSION_CODES.UPSIDE_DOWN_CAKE,
+        oldIsRequestLegacyExternalStorage: Boolean = true,
+        newIsRequestLegacyExternalStorage: Boolean = true,
+        isOldPackageMissing: Boolean = false
+    ) {
+        val parsedPermission = mockParsedPermission(
+            PERMISSION_READ_EXTERNAL_STORAGE,
+            PACKAGE_NAME_0,
+            protectionLevel = PermissionInfo.PROTECTION_DANGEROUS
+        )
+        val oldPackageState = if (isOldPackageMissing) {
+            mockPackageState(APP_ID_0, PACKAGE_NAME_0)
+        } else {
+            mockPackageState(
+                APP_ID_0,
+                mockAndroidPackage(
+                    PACKAGE_NAME_0,
+                    targetSdkVersion = oldTargetSdkVersion,
+                    isRequestLegacyExternalStorage = oldIsRequestLegacyExternalStorage,
+                    requestedPermissions = setOf(PERMISSION_READ_EXTERNAL_STORAGE),
+                    permissions = listOf(parsedPermission)
+                )
+            )
+        }
+        addPackageState(oldPackageState)
+        addPermission(parsedPermission)
+        setPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_READ_EXTERNAL_STORAGE, oldFlags)
+
+        mutateState {
+            val newPackageState = mockPackageState(
+                APP_ID_0,
+                mockAndroidPackage(
+                    PACKAGE_NAME_0,
+                    targetSdkVersion = newTargetSdkVersion,
+                    isRequestLegacyExternalStorage = newIsRequestLegacyExternalStorage,
+                    requestedPermissions = setOf(PERMISSION_READ_EXTERNAL_STORAGE),
+                    permissions = listOf(parsedPermission)
+                )
+            )
+            addPackageState(newPackageState, newState)
+            testAction(newPackageState)
+        }
+    }
+
+    @Test
+    fun testEvaluatePermissionState_normalPermissionRequestedByInstalledPackage_getsGranted() {
+        val oldFlags = PermissionFlags.INSTALL_REVOKED
+        val permissionOwnerPackageState = mockPackageState(APP_ID_0, mockSimpleAndroidPackage())
+        val installedPackageState = mockPackageState(
+            APP_ID_1,
+            mockAndroidPackage(PACKAGE_NAME_1, requestedPermissions = setOf(PERMISSION_NAME_0))
+        )
+        addPackageState(permissionOwnerPackageState)
+        addPackageState(installedPackageState)
+        addPermission(defaultPermission)
+        setPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0, oldFlags)
+
+        mutateState {
+            testAction(installedPackageState)
+        }
+
+        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
+        val expectedNewFlags = PermissionFlags.INSTALL_GRANTED
+        assertWithMessage(
+            "After $action is called for a package that requests a normal permission" +
+                " with the INSTALL_REVOKED flag, the actual permission flags $actualFlags" +
+                " should match the expected flags $expectedNewFlags since it's a new install"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    /**
+     * We set up a permission protection level change from SIGNATURE to NORMAL in order to make
+     * the permission a "changed permission" in order to test evaluatePermissionState() called by
+     * evaluatePermissionStateForAllPackages(). This makes the requestingPackageState not the
+     * installedPackageState so that we can test whether requesting by system package will give us
+     * the expected permission flags.
+     *
+     * Besides, this also helps us test evaluatePermissionStateForAllPackages(). Since both
+     * evaluatePermissionStateForAllPackages() and evaluateAllPermissionStatesForPackage() call
+     * evaluatePermissionState() in their implementations, we use these tests as the only tests
+     * that test evaluatePermissionStateForAllPackages()
+     */
+    @Test
+    fun testEvaluatePermissionState_normalPermissionRequestedBySystemPackage_getsGranted() {
+        testEvaluateNormalPermissionStateWithPermissionChanges(requestingPackageIsSystem = true)
+
+        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
+        val expectedNewFlags = PermissionFlags.INSTALL_GRANTED
+        assertWithMessage(
+            "After $action is called for a system package that requests a normal" +
+                " permission with INSTALL_REVOKED flag, the actual permission flags $actualFlags" +
+                " should match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testEvaluatePermissionState_normalCompatibilityPermission_getsGranted() {
+        testEvaluateNormalPermissionStateWithPermissionChanges(
+            permissionName = PERMISSION_POST_NOTIFICATIONS,
+            requestingPackageTargetSdkVersion = Build.VERSION_CODES.S
+        )
+
+        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_POST_NOTIFICATIONS)
+        val expectedNewFlags = PermissionFlags.INSTALL_GRANTED
+        assertWithMessage(
+            "After $action is called for a package that requests a normal compatibility" +
+                " permission with INSTALL_REVOKED flag, the actual permission flags $actualFlags" +
+                " should match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testEvaluatePermissionState_normalPermissionPreviouslyRevoked_getsInstallRevoked() {
+        testEvaluateNormalPermissionStateWithPermissionChanges()
+
+        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
+        val expectedNewFlags = PermissionFlags.INSTALL_REVOKED
+        assertWithMessage(
+            "After $action is called for a package that requests a normal" +
+                " permission with INSTALL_REVOKED flag, the actual permission flags $actualFlags" +
+                " should match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    private fun testEvaluateNormalPermissionStateWithPermissionChanges(
+        permissionName: String = PERMISSION_NAME_0,
+        requestingPackageTargetSdkVersion: Int = Build.VERSION_CODES.UPSIDE_DOWN_CAKE,
+        requestingPackageIsSystem: Boolean = false
+    ) {
+        val oldParsedPermission = mockParsedPermission(
+            permissionName,
+            PACKAGE_NAME_0,
+            protectionLevel = PermissionInfo.PROTECTION_SIGNATURE
+        )
+        val oldPermissionOwnerPackageState = mockPackageState(
+            APP_ID_0,
+            mockAndroidPackage(PACKAGE_NAME_0, permissions = listOf(oldParsedPermission))
+        )
+        val requestingPackageState = mockPackageState(
+            APP_ID_1,
+            mockAndroidPackage(
+                PACKAGE_NAME_1,
+                requestedPermissions = setOf(permissionName),
+                targetSdkVersion = requestingPackageTargetSdkVersion
+            ),
+            isSystem = requestingPackageIsSystem,
+        )
+        addPackageState(oldPermissionOwnerPackageState)
+        addPackageState(requestingPackageState)
+        addPermission(oldParsedPermission)
+        val oldFlags = PermissionFlags.INSTALL_REVOKED
+        setPermissionFlags(APP_ID_1, USER_ID_0, permissionName, oldFlags)
+
+        mutateState {
+            val newParsedPermission = mockParsedPermission(permissionName, PACKAGE_NAME_0)
+            val newPermissionOwnerPackageState = mockPackageState(
+                APP_ID_0,
+                mockAndroidPackage(PACKAGE_NAME_0, permissions = listOf(newParsedPermission))
+            )
+            addPackageState(newPermissionOwnerPackageState, newState)
+            testAction(newPermissionOwnerPackageState)
+        }
+    }
+
+    private fun MutateStateScope.testAction(packageState: PackageState) {
+        with(appIdPermissionPolicy) {
+            when (action) {
+                Action.ON_PACKAGE_ADDED -> onPackageAdded(packageState)
+                Action.ON_STORAGE_VOLUME_ADDED -> onStorageVolumeMounted(
+                    null,
+                    listOf(packageState.packageName),
+                    true
+                )
+            }
+        }
+    }
+
+    enum class Action { ON_PACKAGE_ADDED, ON_STORAGE_VOLUME_ADDED }
+
+    companion object {
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun data(): Array<Action> = Action.values()
+    }
+}
diff --git a/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionResetTest.kt b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionResetTest.kt
new file mode 100644
index 0000000..823ce45
--- /dev/null
+++ b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionResetTest.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.permission.test
+
+import android.content.pm.PermissionInfo
+import com.android.server.permission.access.MutateStateScope
+import com.android.server.permission.access.permission.PermissionFlags
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * A parameterized test for testing resetting runtime permissions for onPackageUninstalled()
+ * and resetRuntimePermissions() in AppIdPermissionPolicy
+ */
+@RunWith(Parameterized::class)
+class AppIdPermissionPolicyPermissionResetTest : BaseAppIdPermissionPolicyTest() {
+    @Parameterized.Parameter(0) lateinit var action: Action
+
+    @Test
+    fun testResetRuntimePermissions_runtimeGranted_getsRevoked() {
+        val oldFlags = PermissionFlags.RUNTIME_GRANTED
+        val expectedNewFlags = 0
+        testResetRuntimePermissions(oldFlags, expectedNewFlags)
+    }
+
+    @Test
+    fun testResetRuntimePermissions_roleGranted_getsGranted() {
+        val oldFlags = PermissionFlags.ROLE
+        val expectedNewFlags = PermissionFlags.ROLE or PermissionFlags.RUNTIME_GRANTED
+        testResetRuntimePermissions(oldFlags, expectedNewFlags)
+    }
+
+    @Test
+    fun testResetRuntimePermissions_nullAndroidPackage_remainsUnchanged() {
+        val oldFlags = PermissionFlags.RUNTIME_GRANTED
+        val expectedNewFlags = PermissionFlags.RUNTIME_GRANTED
+        testResetRuntimePermissions(oldFlags, expectedNewFlags, isAndroidPackageMissing = true)
+    }
+
+    private fun testResetRuntimePermissions(
+        oldFlags: Int,
+        expectedNewFlags: Int,
+        isAndroidPackageMissing: Boolean = false
+    ) {
+        val parsedPermission = mockParsedPermission(
+            PERMISSION_NAME_0,
+            PACKAGE_NAME_0,
+            protectionLevel = PermissionInfo.PROTECTION_DANGEROUS,
+        )
+        val permissionOwnerPackageState = mockPackageState(
+            APP_ID_0,
+            mockAndroidPackage(PACKAGE_NAME_0, permissions = listOf(parsedPermission))
+        )
+        val requestingPackageState = if (isAndroidPackageMissing) {
+            mockPackageState(APP_ID_1, PACKAGE_NAME_1)
+        } else {
+            mockPackageState(
+                APP_ID_1,
+                mockAndroidPackage(PACKAGE_NAME_1, requestedPermissions = setOf(PERMISSION_NAME_0))
+            )
+        }
+        setPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0, oldFlags)
+        addPackageState(permissionOwnerPackageState)
+        addPackageState(requestingPackageState)
+        addPermission(parsedPermission)
+
+        mutateState { testAction() }
+
+        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
+        assertWithMessage(
+            "After resetting runtime permissions, permission flags did not match" +
+                " expected values: expectedNewFlags is $expectedNewFlags," +
+                " actualFlags is $actualFlags, while the oldFlags is $oldFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    private fun MutateStateScope.testAction(
+        packageName: String = PACKAGE_NAME_1,
+        appId: Int = APP_ID_1,
+        userId: Int = USER_ID_0
+    ) {
+        with(appIdPermissionPolicy) {
+            when (action) {
+                Action.ON_PACKAGE_UNINSTALLED -> onPackageUninstalled(packageName, appId, userId)
+                Action.RESET_RUNTIME_PERMISSIONS -> resetRuntimePermissions(packageName, userId)
+            }
+        }
+    }
+
+    enum class Action { ON_PACKAGE_UNINSTALLED, RESET_RUNTIME_PERMISSIONS }
+
+    companion object {
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun data(): Array<Action> = Action.values()
+    }
+}
\ No newline at end of file
diff --git a/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionStatesTest.kt b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionStatesTest.kt
new file mode 100644
index 0000000..f085bd7
--- /dev/null
+++ b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionStatesTest.kt
@@ -0,0 +1,884 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.permission.test
+
+import android.content.pm.PermissionInfo
+import android.os.Build
+import com.android.server.permission.access.MutableAccessState
+import com.android.server.permission.access.MutateStateScope
+import com.android.server.permission.access.immutable.IndexedListSet
+import com.android.server.permission.access.immutable.MutableIndexedListSet
+import com.android.server.permission.access.immutable.MutableIndexedMap
+import com.android.server.permission.access.permission.PermissionFlags
+import com.android.server.pm.permission.PermissionAllowlist
+import com.android.server.pm.pkg.PackageState
+import com.android.server.testutils.mock
+import com.android.server.testutils.whenever
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * A parameterized test for testing evaluating permission states and inheriting implicit permission
+ * states for onUserAdded(), onStorageVolumeAdded() and onPackageAdded() in AppIdPermissionPolicy
+ */
+@RunWith(Parameterized::class)
+class AppIdPermissionPolicyPermissionStatesTest : BaseAppIdPermissionPolicyTest() {
+    @Parameterized.Parameter(0) lateinit var action: Action
+
+    @Before
+    override fun setUp() {
+        super.setUp()
+        if (action == Action.ON_USER_ADDED) {
+            createUserState(USER_ID_NEW)
+        }
+    }
+
+    @Test
+    fun testEvaluatePermissionState_normalPermissionAlreadyGranted_remainsUnchanged() {
+        val oldFlags = PermissionFlags.INSTALL_GRANTED or PermissionFlags.INSTALL_REVOKED
+        testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_NORMAL) {}
+
+        val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+        val expectedNewFlags = oldFlags
+        assertWithMessage(
+            "After $action is called for a package that requests a normal permission" +
+                " with an existing INSTALL_GRANTED flag, the actual permission flags $actualFlags" +
+                " should match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testEvaluatePermissionState_normalPermissionNotInstallRevoked_getsGranted() {
+        val oldFlags = 0
+        testEvaluatePermissionState(
+            oldFlags,
+            PermissionInfo.PROTECTION_NORMAL,
+            isNewInstall = true
+        ) {}
+
+        val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+        val expectedNewFlags = PermissionFlags.INSTALL_GRANTED
+        assertWithMessage(
+            "After $action is called for a package that requests a normal permission" +
+                " with no existing flags, the actual permission flags $actualFlags" +
+                " should match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testEvaluatePermissionState_normalAppOpPermission_getsRoleAndUserSetFlagsPreserved() {
+        val oldFlags = PermissionFlags.ROLE or PermissionFlags.USER_SET
+        testEvaluatePermissionState(
+            oldFlags,
+            PermissionInfo.PROTECTION_NORMAL or PermissionInfo.PROTECTION_FLAG_APPOP
+        ) {}
+
+        val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+        val expectedNewFlags = PermissionFlags.INSTALL_GRANTED or oldFlags
+        assertWithMessage(
+            "After $action is called for a package that requests a normal app op" +
+                " permission with existing ROLE and USER_SET flags, the actual permission flags" +
+                " $actualFlags should match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testEvaluatePermissionState_internalWasGrantedWithMissingPackage_getsProtectionGranted() {
+        val oldFlags = PermissionFlags.PROTECTION_GRANTED
+        testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_INTERNAL) {
+            val packageStateWithMissingPackage = mockPackageState(APP_ID_1, MISSING_ANDROID_PACKAGE)
+            addPackageState(packageStateWithMissingPackage)
+        }
+
+        val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+        val expectedNewFlags = oldFlags
+        assertWithMessage(
+            "After $action is called for a package that requests an internal permission" +
+                " with missing android package and $oldFlags flag, the actual permission flags" +
+                " $actualFlags should match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testEvaluatePermissionState_internalAppOpPermission_getsRoleAndUserSetFlagsPreserved() {
+        val oldFlags = PermissionFlags.PROTECTION_GRANTED or PermissionFlags.ROLE or
+            PermissionFlags.USER_SET
+        testEvaluatePermissionState(
+            oldFlags,
+            PermissionInfo.PROTECTION_INTERNAL or PermissionInfo.PROTECTION_FLAG_APPOP
+        ) {
+            val packageStateWithMissingPackage = mockPackageState(APP_ID_1, MISSING_ANDROID_PACKAGE)
+            addPackageState(packageStateWithMissingPackage)
+        }
+
+        val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+        val expectedNewFlags = oldFlags
+        assertWithMessage(
+            "After $action is called for a package that requests an internal permission" +
+                " with missing android package and $oldFlags flag and the permission isAppOp," +
+                " the actual permission flags $actualFlags should match the expected" +
+                " flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testEvaluatePermissionState_internalDevelopmentPermission_getsRuntimeGrantedPreserved() {
+        val oldFlags = PermissionFlags.PROTECTION_GRANTED or PermissionFlags.RUNTIME_GRANTED
+        testEvaluatePermissionState(
+            oldFlags,
+            PermissionInfo.PROTECTION_INTERNAL or PermissionInfo.PROTECTION_FLAG_DEVELOPMENT
+        ) {
+            val packageStateWithMissingPackage = mockPackageState(APP_ID_1, MISSING_ANDROID_PACKAGE)
+            addPackageState(packageStateWithMissingPackage)
+        }
+
+        val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+        val expectedNewFlags = oldFlags
+        assertWithMessage(
+            "After $action is called for a package that requests an internal permission" +
+                " with missing android package and $oldFlags flag and permission isDevelopment," +
+                " the actual permission flags $actualFlags should match the expected" +
+                " flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testEvaluatePermissionState_internalRolePermission_getsRoleAndRuntimeGrantedPreserved() {
+        val oldFlags = PermissionFlags.PROTECTION_GRANTED or PermissionFlags.ROLE or
+            PermissionFlags.RUNTIME_GRANTED
+        testEvaluatePermissionState(
+            oldFlags,
+            PermissionInfo.PROTECTION_INTERNAL or PermissionInfo.PROTECTION_FLAG_ROLE
+        ) {
+            val packageStateWithMissingPackage = mockPackageState(APP_ID_1, MISSING_ANDROID_PACKAGE)
+            addPackageState(packageStateWithMissingPackage)
+        }
+
+        val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+        val expectedNewFlags = oldFlags
+        assertWithMessage(
+            "After $action is called for a package that requests an internal permission" +
+                " with missing android package and $oldFlags flag and the permission isRole," +
+                " the actual permission flags $actualFlags should match the expected" +
+                " flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testEvaluatePermissionState_signaturePrivilegedPermissionNotAllowlisted_isNotGranted() {
+        val oldFlags = 0
+        testEvaluatePermissionState(
+            oldFlags,
+            PermissionInfo.PROTECTION_SIGNATURE or PermissionInfo.PROTECTION_FLAG_PRIVILEGED,
+            isInstalledPackageSystem = true,
+            isInstalledPackagePrivileged = true,
+            isInstalledPackageProduct = true,
+            // To mock the return value of shouldGrantPrivilegedOrOemPermission()
+            isInstalledPackageVendor = true,
+            isNewInstall = true
+        ) {
+            val platformPackage = mockPackageState(
+                PLATFORM_APP_ID,
+                mockAndroidPackage(PLATFORM_PACKAGE_NAME)
+            )
+            setupAllowlist(PACKAGE_NAME_1, false)
+            addPackageState(platformPackage)
+        }
+
+        val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+        val expectedNewFlags = oldFlags
+        assertWithMessage(
+            "After $action is called for a package that requests a signature privileged" +
+                " permission that's not allowlisted, the actual permission" +
+                " flags $actualFlags should match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testEvaluatePermissionState_nonPrivilegedShouldGrantBySignature_getsProtectionGranted() {
+        val oldFlags = 0
+        testEvaluatePermissionState(
+            oldFlags,
+            PermissionInfo.PROTECTION_SIGNATURE,
+            isInstalledPackageSystem = true,
+            isInstalledPackagePrivileged = true,
+            isInstalledPackageProduct = true,
+            isInstalledPackageSignatureMatching = true,
+            isInstalledPackageVendor = true,
+            isNewInstall = true
+        ) {
+            val platformPackage = mockPackageState(
+                PLATFORM_APP_ID,
+                mockAndroidPackage(PLATFORM_PACKAGE_NAME, isSignatureMatching = true)
+            )
+            setupAllowlist(PACKAGE_NAME_1, false)
+            addPackageState(platformPackage)
+        }
+
+        val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+        val expectedNewFlags = PermissionFlags.PROTECTION_GRANTED
+        assertWithMessage(
+            "After $action is called for a package that requests a signature" +
+                " non-privileged permission, the actual permission" +
+                " flags $actualFlags should match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testEvaluatePermissionState_privilegedAllowlistShouldGrantByProtectionFlags_getsGranted() {
+        val oldFlags = 0
+        testEvaluatePermissionState(
+            oldFlags,
+            PermissionInfo.PROTECTION_SIGNATURE or PermissionInfo.PROTECTION_FLAG_PRIVILEGED,
+            isInstalledPackageSystem = true,
+            isInstalledPackagePrivileged = true,
+            isInstalledPackageProduct = true,
+            isNewInstall = true
+        ) {
+            val platformPackage = mockPackageState(
+                PLATFORM_APP_ID,
+                mockAndroidPackage(PLATFORM_PACKAGE_NAME)
+            )
+            setupAllowlist(PACKAGE_NAME_1, true)
+            addPackageState(platformPackage)
+        }
+
+        val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+        val expectedNewFlags = PermissionFlags.PROTECTION_GRANTED
+        assertWithMessage(
+            "After $action is called for a package that requests a signature privileged" +
+                " permission that's allowlisted and should grant by protection flags, the actual" +
+                " permission flags $actualFlags should match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    private fun setupAllowlist(
+        packageName: String,
+        allowlistState: Boolean,
+        state: MutableAccessState = oldState
+    ) {
+        state.mutateExternalState().setPrivilegedPermissionAllowlistPackages(
+            MutableIndexedListSet<String>().apply { add(packageName) }
+        )
+        val mockAllowlist = mock<PermissionAllowlist> {
+            whenever(
+                getProductPrivilegedAppAllowlistState(packageName, PERMISSION_NAME_0)
+            ).thenReturn(allowlistState)
+        }
+        state.mutateExternalState().setPermissionAllowlist(mockAllowlist)
+    }
+
+    @Test
+    fun testEvaluatePermissionState_nonRuntimeFlagsOnRuntimePermissions_getsCleared() {
+        val oldFlags = PermissionFlags.INSTALL_GRANTED or PermissionFlags.PREGRANT or
+            PermissionFlags.RUNTIME_GRANTED
+        testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {}
+
+        val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+        val expectedNewFlags = PermissionFlags.PREGRANT or PermissionFlags.RUNTIME_GRANTED
+        assertWithMessage(
+            "After $action is called for a package that requests a runtime permission" +
+                " with existing $oldFlags flags, the actual permission flags $actualFlags should" +
+                " match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testEvaluatePermissionState_newPermissionsForPreM_requiresUserReview() {
+        val oldFlags = 0
+        testEvaluatePermissionState(
+            oldFlags,
+            PermissionInfo.PROTECTION_DANGEROUS,
+            installedPackageTargetSdkVersion = Build.VERSION_CODES.LOLLIPOP,
+            isNewInstall = true
+        ) {}
+
+        val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+        val expectedNewFlags = PermissionFlags.LEGACY_GRANTED or PermissionFlags.IMPLICIT
+        assertWithMessage(
+            "After $action is called for a package that requests a runtime permission" +
+                " with no existing flags in pre M, actual permission flags $actualFlags should" +
+                " match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testEvaluatePermissionState_legacyOrImplicitGrantedPreviouslyRevoked_getsAppOpRevoked() {
+        val oldFlags = PermissionFlags.USER_FIXED
+        testEvaluatePermissionState(
+            oldFlags,
+            PermissionInfo.PROTECTION_DANGEROUS,
+            installedPackageTargetSdkVersion = Build.VERSION_CODES.LOLLIPOP
+        ) {
+            setPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0, oldFlags)
+        }
+
+        val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+        val expectedNewFlags = PermissionFlags.LEGACY_GRANTED or PermissionFlags.USER_FIXED or
+            PermissionFlags.APP_OP_REVOKED
+        assertWithMessage(
+            "After $action is called for a package that requests a runtime permission" +
+                " that should be LEGACY_GRANTED or IMPLICIT_GRANTED that was previously revoked," +
+                " the actual permission flags $actualFlags should" +
+                " match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testEvaluatePermissionState_legacyGrantedForPostM_userReviewRequirementRemoved() {
+        val oldFlags = PermissionFlags.LEGACY_GRANTED or PermissionFlags.IMPLICIT
+        testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {}
+
+        val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+        val expectedNewFlags = 0
+        assertWithMessage(
+            "After $action is called for a package that requests a runtime permission" +
+                " that used to require user review, the user review requirement should be removed" +
+                " if it's upgraded to post M. The actual permission flags $actualFlags should" +
+                " match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testEvaluatePermissionState_legacyGrantedPermissionsAlreadyReviewedForPostM_getsGranted() {
+        val oldFlags = PermissionFlags.LEGACY_GRANTED
+        testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {}
+
+        val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+        val expectedNewFlags = PermissionFlags.RUNTIME_GRANTED
+        assertWithMessage(
+            "After $action is called for a package that requests a runtime permission" +
+                " that was already reviewed by the user, the permission should be RUNTIME_GRANTED" +
+                " if it's upgraded to post M. The actual permission flags $actualFlags should" +
+                " match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testEvaluatePermissionState_leanbackNotificationPermissionsForPostM_getsImplicitGranted() {
+        val oldFlags = 0
+        testEvaluatePermissionState(
+            oldFlags,
+            PermissionInfo.PROTECTION_DANGEROUS,
+            permissionName = PERMISSION_POST_NOTIFICATIONS,
+            isNewInstall = true
+        ) {
+            oldState.mutateExternalState().setLeanback(true)
+        }
+
+        val actualFlags = getPermissionFlags(
+            APP_ID_1,
+            getUserIdEvaluated(),
+            PERMISSION_POST_NOTIFICATIONS
+        )
+        val expectedNewFlags = PermissionFlags.IMPLICIT_GRANTED
+        assertWithMessage(
+            "After $action is called for a package that requests a runtime notification" +
+                " permission when isLeanback, the actual permission flags $actualFlags should" +
+                " match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testEvaluatePermissionState_implicitSourceFromNonRuntime_getsImplicitGranted() {
+        val oldFlags = 0
+        testEvaluatePermissionState(
+            oldFlags,
+            PermissionInfo.PROTECTION_DANGEROUS,
+            implicitPermissions = setOf(PERMISSION_NAME_0),
+            isNewInstall = true
+        ) {
+            oldState.mutateExternalState().setImplicitToSourcePermissions(
+                MutableIndexedMap<String, IndexedListSet<String>>().apply {
+                    put(PERMISSION_NAME_0, MutableIndexedListSet<String>().apply {
+                        add(PERMISSION_NAME_1)
+                    })
+                }
+            )
+            addPermission(mockParsedPermission(PERMISSION_NAME_1, PACKAGE_NAME_0))
+        }
+
+        val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+        val expectedNewFlags = PermissionFlags.IMPLICIT_GRANTED or PermissionFlags.IMPLICIT
+        assertWithMessage(
+            "After $action is called for a package that requests a runtime implicit" +
+                " permission that's source from a non-runtime permission, the actual permission" +
+                " flags $actualFlags should match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    /**
+     * For a legacy granted or implicit permission during the app upgrade, when the permission
+     * should no longer be legacy or implicit granted, we want to remove the APP_OP_REVOKED flag
+     * so that the app can request the permission.
+     */
+    @Test
+    fun testEvaluatePermissionState_noLongerLegacyOrImplicitGranted_canBeRequested() {
+        val oldFlags = PermissionFlags.LEGACY_GRANTED or PermissionFlags.APP_OP_REVOKED or
+            PermissionFlags.RUNTIME_GRANTED
+        testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {}
+
+        val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+        val expectedNewFlags = 0
+        assertWithMessage(
+            "After $action is called for a package that requests a runtime permission" +
+                " that is no longer LEGACY_GRANTED or IMPLICIT_GRANTED, the actual permission" +
+                " flags $actualFlags should match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testEvaluatePermissionState_noLongerImplicit_getsRuntimeAndImplicitFlagsRemoved() {
+        val oldFlags = PermissionFlags.IMPLICIT or PermissionFlags.RUNTIME_GRANTED or
+            PermissionFlags.USER_SET or PermissionFlags.USER_FIXED
+        testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {}
+
+        val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+        val expectedNewFlags = 0
+        assertWithMessage(
+            "After $action is called for a package that requests a runtime permission" +
+                " that is no longer implicit and we shouldn't retain as nearby device" +
+                " permissions, the actual permission flags $actualFlags should match the expected" +
+                " flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testEvaluatePermissionState_noLongerImplicitNearbyWasGranted_getsRuntimeGranted() {
+        val oldFlags = PermissionFlags.IMPLICIT_GRANTED or PermissionFlags.IMPLICIT
+        testEvaluatePermissionState(
+            oldFlags,
+            PermissionInfo.PROTECTION_DANGEROUS,
+            permissionName = PERMISSION_BLUETOOTH_CONNECT,
+            requestedPermissions = setOf(
+                PERMISSION_BLUETOOTH_CONNECT,
+                PERMISSION_ACCESS_BACKGROUND_LOCATION
+            )
+        ) {
+            setPermissionFlags(
+                APP_ID_1,
+                getUserIdEvaluated(),
+                PERMISSION_ACCESS_BACKGROUND_LOCATION,
+                PermissionFlags.RUNTIME_GRANTED
+            )
+        }
+
+        val actualFlags = getPermissionFlags(
+            APP_ID_1,
+            getUserIdEvaluated(),
+            PERMISSION_BLUETOOTH_CONNECT
+        )
+        val expectedNewFlags = PermissionFlags.RUNTIME_GRANTED
+        assertWithMessage(
+            "After $action is called for a package that requests a runtime nearby device" +
+                " permission that was granted by implicit, the actual permission flags" +
+                " $actualFlags should match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testEvaluatePermissionState_noLongerImplicitSystemOrPolicyFixedWasGranted_runtimeGranted() {
+        val oldFlags = PermissionFlags.IMPLICIT_GRANTED or PermissionFlags.IMPLICIT or
+            PermissionFlags.SYSTEM_FIXED
+        testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {}
+
+        val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+        val expectedNewFlags = PermissionFlags.RUNTIME_GRANTED or PermissionFlags.SYSTEM_FIXED
+        assertWithMessage(
+            "After $action is called for a package that requests a runtime permission" +
+                " that was granted and is no longer implicit and is SYSTEM_FIXED or POLICY_FIXED," +
+                " the actual permission flags $actualFlags should match the expected" +
+                " flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testEvaluatePermissionState_restrictedPermissionsNotExempt_getsRestrictionFlags() {
+        val oldFlags = PermissionFlags.RESTRICTION_REVOKED
+        testEvaluatePermissionState(
+            oldFlags,
+            PermissionInfo.PROTECTION_DANGEROUS,
+            permissionInfoFlags = PermissionInfo.FLAG_HARD_RESTRICTED
+        ) {}
+
+        val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+        val expectedNewFlags = oldFlags
+        assertWithMessage(
+            "After $action is called for a package that requests a runtime hard" +
+                " restricted permission that is not exempted, the actual permission flags" +
+                " $actualFlags should match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testEvaluatePermissionState_restrictedPermissionsIsExempted_clearsRestrictionFlags() {
+        val oldFlags = 0
+        testEvaluatePermissionState(
+            oldFlags,
+            PermissionInfo.PROTECTION_DANGEROUS,
+            permissionInfoFlags = PermissionInfo.FLAG_SOFT_RESTRICTED
+        ) {}
+
+        val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+        val expectedNewFlags = PermissionFlags.UPGRADE_EXEMPT
+        assertWithMessage(
+            "After $action is called for a package that requests a runtime soft" +
+                " restricted permission that is exempted, the actual permission flags" +
+                " $actualFlags should match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testInheritImplicitPermissionStates_runtimeExistingImplicit_sourceFlagsNotInherited() {
+        val oldImplicitPermissionFlags = PermissionFlags.USER_FIXED
+        testInheritImplicitPermissionStates(
+            implicitPermissionFlags = oldImplicitPermissionFlags,
+            isNewInstallAndNewPermission = false
+        )
+
+        val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+        val expectedNewFlags = oldImplicitPermissionFlags or PermissionFlags.IMPLICIT_GRANTED or
+            PermissionFlags.APP_OP_REVOKED
+        assertWithMessage(
+            "After $action is called for a package that requests a permission that is" +
+                " implicit, existing and runtime, it should not inherit the runtime flags from" +
+                " the source permission. Hence the actual permission flags $actualFlags should" +
+                " match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testInheritImplicitPermissionStates_nonRuntimeNewImplicit_sourceFlagsNotInherited() {
+        testInheritImplicitPermissionStates(
+            implicitPermissionProtectionLevel = PermissionInfo.PROTECTION_NORMAL
+        )
+
+        val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+        val expectedNewFlags = PermissionFlags.INSTALL_GRANTED
+        assertWithMessage(
+            "After $action is called for a package that requests a permission that is" +
+                " implicit, new and non-runtime, it should not inherit the runtime flags from" +
+                " the source permission. Hence the actual permission flags $actualFlags should" +
+                " match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testInheritImplicitPermissionStates_runtimeNewImplicitPermissions_sourceFlagsInherited() {
+        val sourceRuntimeFlags = PermissionFlags.RUNTIME_GRANTED or PermissionFlags.USER_SET
+        testInheritImplicitPermissionStates(sourceRuntimeFlags = sourceRuntimeFlags)
+
+        val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+        val expectedNewFlags = sourceRuntimeFlags or PermissionFlags.IMPLICIT_GRANTED or
+            PermissionFlags.IMPLICIT
+        assertWithMessage(
+            "After $action is called for a package that requests a permission that is" +
+                " implicit, new and runtime, it should inherit the runtime flags from" +
+                " the source permission. Hence the actual permission flags $actualFlags should" +
+                " match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testInheritImplicitPermissionStates_grantingNewFromRevokeImplicit_onlyInheritFromSource() {
+        val sourceRuntimeFlags = PermissionFlags.RUNTIME_GRANTED or PermissionFlags.USER_SET
+        testInheritImplicitPermissionStates(
+            implicitPermissionFlags = PermissionFlags.POLICY_FIXED,
+            sourceRuntimeFlags = sourceRuntimeFlags,
+            isAnySourcePermissionNonRuntime = false
+        )
+
+        val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+        val expectedNewFlags = sourceRuntimeFlags or PermissionFlags.IMPLICIT
+        assertWithMessage(
+            "After $action is called for a package that requests a permission that is" +
+                " implicit, existing, runtime and revoked, it should only inherit runtime flags" +
+                " from source permission. Hence the actual permission flags $actualFlags should" +
+                " match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    /**
+     * If it's a media implicit permission (one of RETAIN_IMPLICIT_FLAGS_PERMISSIONS), we want to
+     * remove the IMPLICIT flag so that they will be granted when they are no longer implicit.
+     * (instead of revoking it)
+     */
+    @Test
+    fun testInheritImplicitPermissionStates_mediaImplicitPermissions_getsImplicitFlagRemoved() {
+        val sourceRuntimeFlags = PermissionFlags.RUNTIME_GRANTED or PermissionFlags.USER_SET
+        testInheritImplicitPermissionStates(
+            implicitPermissionName = PERMISSION_ACCESS_MEDIA_LOCATION,
+            sourceRuntimeFlags = sourceRuntimeFlags
+        )
+
+        val actualFlags = getPermissionFlags(
+            APP_ID_1,
+            getUserIdEvaluated(),
+            PERMISSION_ACCESS_MEDIA_LOCATION
+        )
+        val expectedNewFlags = sourceRuntimeFlags or PermissionFlags.IMPLICIT_GRANTED
+        assertWithMessage(
+            "After $action is called for a package that requests a media permission that" +
+                " is implicit, new and runtime, it should inherit the runtime flags from" +
+                " the source permission and have the IMPLICIT flag removed. Hence the actual" +
+                " permission flags $actualFlags should match the expected flags $expectedNewFlags"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    private fun testInheritImplicitPermissionStates(
+        implicitPermissionName: String = PERMISSION_NAME_0,
+        implicitPermissionFlags: Int = 0,
+        implicitPermissionProtectionLevel: Int = PermissionInfo.PROTECTION_DANGEROUS,
+        sourceRuntimeFlags: Int = PermissionFlags.RUNTIME_GRANTED or PermissionFlags.USER_SET,
+        isAnySourcePermissionNonRuntime: Boolean = true,
+        isNewInstallAndNewPermission: Boolean = true
+    ) {
+        val userId = getUserIdEvaluated()
+        val implicitPermission = mockParsedPermission(
+            implicitPermissionName,
+            PACKAGE_NAME_0,
+            protectionLevel = implicitPermissionProtectionLevel,
+        )
+        // For source from non-runtime in order to grant by implicit
+        val sourcePermission1 = mockParsedPermission(
+            PERMISSION_NAME_1,
+            PACKAGE_NAME_0,
+            protectionLevel = if (isAnySourcePermissionNonRuntime) {
+                PermissionInfo.PROTECTION_NORMAL
+            } else {
+                PermissionInfo.PROTECTION_DANGEROUS
+            }
+        )
+        // For inheriting runtime flags
+        val sourcePermission2 = mockParsedPermission(
+            PERMISSION_NAME_2,
+            PACKAGE_NAME_0,
+            protectionLevel = PermissionInfo.PROTECTION_DANGEROUS,
+        )
+        val permissionOwnerPackageState = mockPackageState(
+            APP_ID_0,
+            mockAndroidPackage(
+                PACKAGE_NAME_0,
+                permissions = listOf(implicitPermission, sourcePermission1, sourcePermission2)
+            )
+        )
+        val installedPackageState = mockPackageState(
+            APP_ID_1,
+            mockAndroidPackage(
+                PACKAGE_NAME_1,
+                requestedPermissions = setOf(
+                    implicitPermissionName,
+                    PERMISSION_NAME_1,
+                    PERMISSION_NAME_2
+                ),
+                implicitPermissions = setOf(implicitPermissionName)
+            )
+        )
+        oldState.mutateExternalState().setImplicitToSourcePermissions(
+            MutableIndexedMap<String, IndexedListSet<String>>().apply {
+                put(implicitPermissionName, MutableIndexedListSet<String>().apply {
+                    add(PERMISSION_NAME_1)
+                    add(PERMISSION_NAME_2)
+                })
+            }
+        )
+        addPackageState(permissionOwnerPackageState)
+        addPermission(implicitPermission)
+        addPermission(sourcePermission1)
+        addPermission(sourcePermission2)
+        if (!isNewInstallAndNewPermission) {
+            addPackageState(installedPackageState)
+            setPermissionFlags(APP_ID_1, userId, implicitPermissionName, implicitPermissionFlags)
+        }
+        setPermissionFlags(APP_ID_1, userId, PERMISSION_NAME_2, sourceRuntimeFlags)
+
+        mutateState {
+            if (isNewInstallAndNewPermission) {
+                addPackageState(installedPackageState)
+                setPermissionFlags(
+                    APP_ID_1,
+                    userId,
+                    implicitPermissionName,
+                    implicitPermissionFlags,
+                    newState
+                )
+            }
+            testAction(installedPackageState)
+        }
+    }
+
+    /**
+     * Setup simple package states for testing evaluatePermissionState().
+     * permissionOwnerPackageState is definer of permissionName with APP_ID_0.
+     * installedPackageState is the installed package that requests permissionName with APP_ID_1.
+     *
+     * @param oldFlags the existing permission flags for APP_ID_1, userId, permissionName
+     * @param protectionLevel the protectionLevel for the permission
+     * @param permissionName the name of the permission (1) being defined (2) of the oldFlags, and
+     *                       (3) requested by installedPackageState
+     * @param requestedPermissions the permissions requested by installedPackageState
+     * @param implicitPermissions the implicit permissions of installedPackageState
+     * @param permissionInfoFlags the flags for the permission itself
+     * @param isInstalledPackageSystem whether installedPackageState is a system package
+     *
+     * @return installedPackageState
+     */
+    private fun testEvaluatePermissionState(
+        oldFlags: Int,
+        protectionLevel: Int,
+        permissionName: String = PERMISSION_NAME_0,
+        requestedPermissions: Set<String> = setOf(permissionName),
+        implicitPermissions: Set<String> = emptySet(),
+        permissionInfoFlags: Int = 0,
+        isInstalledPackageSystem: Boolean = false,
+        isInstalledPackagePrivileged: Boolean = false,
+        isInstalledPackageProduct: Boolean = false,
+        isInstalledPackageSignatureMatching: Boolean = false,
+        isInstalledPackageVendor: Boolean = false,
+        installedPackageTargetSdkVersion: Int = Build.VERSION_CODES.UPSIDE_DOWN_CAKE,
+        isNewInstall: Boolean = false,
+        additionalSetup: () -> Unit
+    ) {
+        val userId = getUserIdEvaluated()
+        val parsedPermission = mockParsedPermission(
+            permissionName,
+            PACKAGE_NAME_0,
+            protectionLevel = protectionLevel,
+            flags = permissionInfoFlags
+        )
+        val permissionOwnerPackageState = mockPackageState(
+            APP_ID_0,
+            mockAndroidPackage(PACKAGE_NAME_0, permissions = listOf(parsedPermission))
+        )
+        val installedPackageState = mockPackageState(
+            APP_ID_1,
+            mockAndroidPackage(
+                PACKAGE_NAME_1,
+                requestedPermissions = requestedPermissions,
+                implicitPermissions = implicitPermissions,
+                targetSdkVersion = installedPackageTargetSdkVersion,
+                isSignatureMatching = isInstalledPackageSignatureMatching
+            ),
+            isSystem = isInstalledPackageSystem,
+            isPrivileged = isInstalledPackagePrivileged,
+            isProduct = isInstalledPackageProduct,
+            isVendor = isInstalledPackageVendor
+        )
+        addPackageState(permissionOwnerPackageState)
+        if (!isNewInstall) {
+            addPackageState(installedPackageState)
+            setPermissionFlags(APP_ID_1, userId, permissionName, oldFlags)
+        }
+        addPermission(parsedPermission)
+
+        additionalSetup()
+
+        mutateState {
+            if (isNewInstall) {
+                addPackageState(installedPackageState, newState)
+                setPermissionFlags(APP_ID_1, userId, permissionName, oldFlags, newState)
+            }
+            testAction(installedPackageState)
+        }
+    }
+
+    private fun getUserIdEvaluated(): Int = when (action) {
+        Action.ON_USER_ADDED -> USER_ID_NEW
+        Action.ON_STORAGE_VOLUME_ADDED, Action.ON_PACKAGE_ADDED -> USER_ID_0
+    }
+
+    private fun MutateStateScope.testAction(packageState: PackageState) {
+        with(appIdPermissionPolicy) {
+            when (action) {
+                Action.ON_USER_ADDED -> onUserAdded(getUserIdEvaluated())
+                Action.ON_STORAGE_VOLUME_ADDED -> onStorageVolumeMounted(
+                    null,
+                    listOf(packageState.packageName),
+                    true
+                )
+                Action.ON_PACKAGE_ADDED -> onPackageAdded(packageState)
+            }
+        }
+    }
+
+    enum class Action { ON_USER_ADDED, ON_STORAGE_VOLUME_ADDED, ON_PACKAGE_ADDED }
+
+    companion object {
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun data(): Array<Action> = Action.values()
+    }
+}
\ No newline at end of file
diff --git a/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyTest.kt b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyTest.kt
deleted file mode 100644
index 3cf57a3..0000000
--- a/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyTest.kt
+++ /dev/null
@@ -1,1937 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.permission.test
-
-import android.Manifest
-import android.content.pm.PackageManager
-import android.content.pm.PermissionGroupInfo
-import android.content.pm.PermissionInfo
-import android.content.pm.SigningDetails
-import android.os.Build
-import android.os.Bundle
-import android.util.ArrayMap
-import android.util.SparseArray
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.modules.utils.testing.ExtendedMockitoRule
-import com.android.server.extendedtestutils.wheneverStatic
-import com.android.server.permission.access.MutableAccessState
-import com.android.server.permission.access.MutableUserState
-import com.android.server.permission.access.MutateStateScope
-import com.android.server.permission.access.immutable.* // ktlint-disable no-wildcard-imports
-import com.android.server.permission.access.permission.AppIdPermissionPolicy
-import com.android.server.permission.access.permission.Permission
-import com.android.server.permission.access.permission.PermissionFlags
-import com.android.server.permission.access.util.hasBits
-import com.android.server.pm.parsing.PackageInfoUtils
-import com.android.server.pm.permission.PermissionAllowlist
-import com.android.server.pm.pkg.AndroidPackage
-import com.android.server.pm.pkg.PackageState
-import com.android.server.pm.pkg.PackageUserState
-import com.android.server.pm.pkg.component.ParsedPermission
-import com.android.server.pm.pkg.component.ParsedPermissionGroup
-import com.android.server.testutils.any
-import com.android.server.testutils.mock
-import com.android.server.testutils.whenever
-import com.google.common.truth.Truth.assertWithMessage
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.anyLong
-
-/**
- * Mocking unit test for AppIdPermissionPolicy.
- */
-@RunWith(AndroidJUnit4::class)
-class AppIdPermissionPolicyTest {
-    private lateinit var oldState: MutableAccessState
-    private lateinit var newState: MutableAccessState
-
-    private val defaultPermissionGroup = mockParsedPermissionGroup(
-        PERMISSION_GROUP_NAME_0,
-        PACKAGE_NAME_0
-    )
-    private val defaultPermissionTree = mockParsedPermission(
-        PERMISSION_TREE_NAME,
-        PACKAGE_NAME_0,
-        isTree = true
-    )
-    private val defaultPermission = mockParsedPermission(PERMISSION_NAME_0, PACKAGE_NAME_0)
-
-    private val appIdPermissionPolicy = AppIdPermissionPolicy()
-
-    @Rule
-    @JvmField
-    val extendedMockitoRule = ExtendedMockitoRule.Builder(this)
-        .spyStatic(PackageInfoUtils::class.java)
-        .build()
-
-    @Before
-    fun setUp() {
-        oldState = MutableAccessState()
-        createUserState(USER_ID_0)
-        oldState.mutateExternalState().setPackageStates(ArrayMap())
-        oldState.mutateExternalState().setDisabledSystemPackageStates(ArrayMap())
-        mockPackageInfoUtilsGeneratePermissionInfo()
-        mockPackageInfoUtilsGeneratePermissionGroupInfo()
-    }
-
-    private fun createUserState(userId: Int) {
-        oldState.mutateExternalState().mutateUserIds().add(userId)
-        oldState.mutateUserStatesNoWrite().put(userId, MutableUserState())
-    }
-
-    private fun mockPackageInfoUtilsGeneratePermissionInfo() {
-        wheneverStatic {
-            PackageInfoUtils.generatePermissionInfo(any(ParsedPermission::class.java), anyLong())
-        }.thenAnswer { invocation ->
-            val parsedPermission = invocation.getArgument<ParsedPermission>(0)
-            val generateFlags = invocation.getArgument<Long>(1)
-            PermissionInfo(parsedPermission.backgroundPermission).apply {
-                name = parsedPermission.name
-                packageName = parsedPermission.packageName
-                metaData = if (generateFlags.toInt().hasBits(PackageManager.GET_META_DATA)) {
-                    parsedPermission.metaData
-                } else {
-                    null
-                }
-                @Suppress("DEPRECATION")
-                protectionLevel = parsedPermission.protectionLevel
-                group = parsedPermission.group
-                flags = parsedPermission.flags
-            }
-        }
-    }
-
-    private fun mockPackageInfoUtilsGeneratePermissionGroupInfo() {
-        wheneverStatic {
-            PackageInfoUtils.generatePermissionGroupInfo(
-                any(ParsedPermissionGroup::class.java),
-                anyLong()
-            )
-        }.thenAnswer { invocation ->
-            val parsedPermissionGroup = invocation.getArgument<ParsedPermissionGroup>(0)
-            val generateFlags = invocation.getArgument<Long>(1)
-            @Suppress("DEPRECATION")
-            PermissionGroupInfo().apply {
-                name = parsedPermissionGroup.name
-                packageName = parsedPermissionGroup.packageName
-                metaData = if (generateFlags.toInt().hasBits(PackageManager.GET_META_DATA)) {
-                    parsedPermissionGroup.metaData
-                } else {
-                    null
-                }
-                flags = parsedPermissionGroup.flags
-            }
-        }
-    }
-
-    @Test
-    fun testResetRuntimePermissions_runtimeGranted_getsRevoked() {
-        val oldFlags = PermissionFlags.RUNTIME_GRANTED
-        val expectedNewFlags = 0
-        testResetRuntimePermissions(oldFlags, expectedNewFlags)
-    }
-
-    @Test
-    fun testResetRuntimePermissions_roleGranted_getsGranted() {
-        val oldFlags = PermissionFlags.ROLE
-        val expectedNewFlags = PermissionFlags.ROLE or PermissionFlags.RUNTIME_GRANTED
-        testResetRuntimePermissions(oldFlags, expectedNewFlags)
-    }
-
-    @Test
-    fun testResetRuntimePermissions_nullAndroidPackage_remainsUnchanged() {
-        val oldFlags = PermissionFlags.RUNTIME_GRANTED
-        val expectedNewFlags = PermissionFlags.RUNTIME_GRANTED
-        testResetRuntimePermissions(oldFlags, expectedNewFlags, isAndroidPackageMissing = true)
-    }
-
-    private fun testResetRuntimePermissions(
-        oldFlags: Int,
-        expectedNewFlags: Int,
-        isAndroidPackageMissing: Boolean = false
-    ) {
-        val parsedPermission = mockParsedPermission(
-            PERMISSION_NAME_0,
-            PACKAGE_NAME_0,
-            protectionLevel = PermissionInfo.PROTECTION_DANGEROUS,
-        )
-        val permissionOwnerPackageState = mockPackageState(
-            APP_ID_0,
-            mockAndroidPackage(PACKAGE_NAME_0, permissions = listOf(parsedPermission))
-        )
-        val requestingPackageState = if (isAndroidPackageMissing) {
-            mockPackageState(APP_ID_1, PACKAGE_NAME_1)
-        } else {
-            mockPackageState(
-                APP_ID_1,
-                mockAndroidPackage(PACKAGE_NAME_1, requestedPermissions = setOf(PERMISSION_NAME_0))
-            )
-        }
-        setPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0, oldFlags)
-        addPackageState(permissionOwnerPackageState)
-        addPackageState(requestingPackageState)
-        addPermission(parsedPermission)
-
-        mutateState {
-            with(appIdPermissionPolicy) {
-                resetRuntimePermissions(PACKAGE_NAME_1, USER_ID_0)
-            }
-        }
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        assertWithMessage(
-            "After resetting runtime permissions, permission flags did not match" +
-                " expected values: expectedNewFlags is $expectedNewFlags," +
-                " actualFlags is $actualFlags, while the oldFlags is $oldFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_permissionsOfMissingSystemApp_getsAdopted() {
-        testAdoptPermissions(hasMissingPackage = true, isSystem = true)
-
-        assertWithMessage(
-            "After onPackageAdded() is called for a null adopt permission package," +
-                " the permission package name: ${getPermission(PERMISSION_NAME_0)?.packageName}" +
-                " did not match the expected package name: $PACKAGE_NAME_0"
-        )
-            .that(getPermission(PERMISSION_NAME_0)?.packageName)
-            .isEqualTo(PACKAGE_NAME_0)
-    }
-
-    @Test
-    fun testOnPackageAdded_permissionsOfExistingSystemApp_notAdopted() {
-        testAdoptPermissions(isSystem = true)
-
-        assertWithMessage(
-            "After onPackageAdded() is called for a non-null adopt permission" +
-                " package, the permission package name:" +
-                " ${getPermission(PERMISSION_NAME_0)?.packageName} should not match the" +
-                " package name: $PACKAGE_NAME_0"
-        )
-            .that(getPermission(PERMISSION_NAME_0)?.packageName)
-            .isNotEqualTo(PACKAGE_NAME_0)
-    }
-
-    @Test
-    fun testOnPackageAdded_permissionsOfNonSystemApp_notAdopted() {
-        testAdoptPermissions(hasMissingPackage = true)
-
-        assertWithMessage(
-            "After onPackageAdded() is called for a non-system adopt permission" +
-                " package, the permission package name:" +
-                " ${getPermission(PERMISSION_NAME_0)?.packageName} should not match the" +
-                " package name: $PACKAGE_NAME_0"
-        )
-            .that(getPermission(PERMISSION_NAME_0)?.packageName)
-            .isNotEqualTo(PACKAGE_NAME_0)
-    }
-
-    private fun testAdoptPermissions(
-        hasMissingPackage: Boolean = false,
-        isSystem: Boolean = false
-    ) {
-        val parsedPermission = mockParsedPermission(PERMISSION_NAME_0, PACKAGE_NAME_1)
-        val packageToAdoptPermission = if (hasMissingPackage) {
-            mockPackageState(APP_ID_1, PACKAGE_NAME_1, isSystem = isSystem)
-        } else {
-            mockPackageState(
-                APP_ID_1,
-                mockAndroidPackage(
-                    PACKAGE_NAME_1,
-                    permissions = listOf(parsedPermission)
-                ),
-                isSystem = isSystem
-            )
-        }
-        addPackageState(packageToAdoptPermission)
-        addPermission(parsedPermission)
-
-        mutateState {
-            val installedPackage = mockPackageState(
-                APP_ID_0,
-                mockAndroidPackage(
-                    PACKAGE_NAME_0,
-                    permissions = listOf(defaultPermission),
-                    adoptPermissions = listOf(PACKAGE_NAME_1)
-                )
-            )
-            addPackageState(installedPackage, newState)
-            with(appIdPermissionPolicy) {
-                onPackageAdded(installedPackage)
-            }
-        }
-    }
-
-    @Test
-    fun testOnPackageAdded_newPermissionGroup_getsDeclared() {
-        mutateState {
-            val packageState = mockPackageState(APP_ID_0, mockSimpleAndroidPackage())
-            addPackageState(packageState, newState)
-            with(appIdPermissionPolicy) {
-                onPackageAdded(packageState)
-            }
-        }
-
-        assertWithMessage(
-            "After onPackageAdded() is called when there is no existing" +
-                " permission groups, the new permission group $PERMISSION_GROUP_NAME_0 is not added"
-        )
-            .that(getPermissionGroup(PERMISSION_GROUP_NAME_0)?.name)
-            .isEqualTo(PERMISSION_GROUP_NAME_0)
-    }
-
-    @Test
-    fun testOnPackageAdded_systemAppTakingOverPermissionGroupDefinition_getsTakenOver() {
-        testTakingOverPermissionAndPermissionGroupDefinitions(newPermissionOwnerIsSystem = true)
-
-        assertWithMessage(
-            "After onPackageAdded() is called when $PERMISSION_GROUP_NAME_0 already" +
-                " exists in the system, the system app $PACKAGE_NAME_0 didn't takeover the" +
-                " ownership of this permission group"
-        )
-            .that(getPermissionGroup(PERMISSION_GROUP_NAME_0)?.packageName)
-            .isEqualTo(PACKAGE_NAME_0)
-    }
-
-    @Test
-    fun testOnPackageAdded_instantApps_remainsUnchanged() {
-        testTakingOverPermissionAndPermissionGroupDefinitions(
-            newPermissionOwnerIsInstant = true,
-            permissionGroupAlreadyExists = false
-        )
-
-        assertWithMessage(
-            "After onPackageAdded() is called for an instant app," +
-                " the new permission group $PERMISSION_GROUP_NAME_0 should not be added"
-        )
-            .that(getPermissionGroup(PERMISSION_GROUP_NAME_0))
-            .isNull()
-    }
-
-    @Test
-    fun testOnPackageAdded_nonSystemAppTakingOverPermissionGroupDefinition_remainsUnchanged() {
-        testTakingOverPermissionAndPermissionGroupDefinitions()
-
-        assertWithMessage(
-            "After onPackageAdded() is called when $PERMISSION_GROUP_NAME_0 already" +
-                " exists in the system, non-system app $PACKAGE_NAME_0 shouldn't takeover" +
-                " ownership of this permission group"
-        )
-            .that(getPermissionGroup(PERMISSION_GROUP_NAME_0)?.packageName)
-            .isEqualTo(PACKAGE_NAME_1)
-    }
-
-    @Test
-    fun testOnPackageAdded_takingOverPermissionGroupDeclaredBySystemApp_remainsUnchanged() {
-        testTakingOverPermissionAndPermissionGroupDefinitions(oldPermissionOwnerIsSystem = true)
-
-        assertWithMessage(
-            "After onPackageAdded() is called when $PERMISSION_GROUP_NAME_0 already" +
-                " exists in the system and is owned by a system app, app $PACKAGE_NAME_0" +
-                " shouldn't takeover ownership of this permission group"
-        )
-            .that(getPermissionGroup(PERMISSION_GROUP_NAME_0)?.packageName)
-            .isEqualTo(PACKAGE_NAME_1)
-    }
-
-    @Test
-    fun testOnPackageAdded_newPermission_getsDeclared() {
-        mutateState {
-            val packageState = mockPackageState(APP_ID_0, mockSimpleAndroidPackage())
-            addPackageState(packageState, newState)
-            with(appIdPermissionPolicy) {
-                onPackageAdded(packageState)
-            }
-        }
-
-        assertWithMessage(
-            "After onPackageAdded() is called when there is no existing" +
-                " permissions, the new permission $PERMISSION_NAME_0 is not added"
-        )
-            .that(getPermission(PERMISSION_NAME_0)?.name)
-            .isEqualTo(PERMISSION_NAME_0)
-    }
-
-    @Test
-    fun testOnPackageAdded_configPermission_getsTakenOver() {
-        testTakingOverPermissionAndPermissionGroupDefinitions(
-            oldPermissionOwnerIsSystem = true,
-            newPermissionOwnerIsSystem = true,
-            type = Permission.TYPE_CONFIG,
-            isReconciled = false
-        )
-
-        assertWithMessage(
-            "After onPackageAdded() is called for a config permission with" +
-                " no owner, the ownership is not taken over by a system app $PACKAGE_NAME_0"
-        )
-            .that(getPermission(PERMISSION_NAME_0)?.packageName)
-            .isEqualTo(PACKAGE_NAME_0)
-    }
-
-    @Test
-    fun testOnPackageAdded_systemAppTakingOverPermissionDefinition_getsTakenOver() {
-        testTakingOverPermissionAndPermissionGroupDefinitions(newPermissionOwnerIsSystem = true)
-
-        assertWithMessage(
-            "After onPackageAdded() is called when $PERMISSION_NAME_0 already" +
-                " exists in the system, the system app $PACKAGE_NAME_0 didn't takeover ownership" +
-                " of this permission"
-        )
-            .that(getPermission(PERMISSION_NAME_0)?.packageName)
-            .isEqualTo(PACKAGE_NAME_0)
-    }
-
-    @Test
-    fun testOnPackageAdded_nonSystemAppTakingOverPermissionDefinition_remainsUnchanged() {
-        testTakingOverPermissionAndPermissionGroupDefinitions()
-
-        assertWithMessage(
-            "After onPackageAdded() is called when $PERMISSION_NAME_0 already" +
-                " exists in the system, the non-system app $PACKAGE_NAME_0 shouldn't takeover" +
-                " ownership of this permission"
-        )
-            .that(getPermission(PERMISSION_NAME_0)?.packageName)
-            .isEqualTo(PACKAGE_NAME_1)
-    }
-
-    @Test
-    fun testOnPackageAdded_takingOverPermissionDeclaredBySystemApp_remainsUnchanged() {
-        testTakingOverPermissionAndPermissionGroupDefinitions(oldPermissionOwnerIsSystem = true)
-
-        assertWithMessage(
-            "After onPackageAdded() is called when $PERMISSION_NAME_0 already" +
-                " exists in system and is owned by a system app, the $PACKAGE_NAME_0 shouldn't" +
-                " takeover ownership of this permission"
-        )
-            .that(getPermission(PERMISSION_NAME_0)?.packageName)
-            .isEqualTo(PACKAGE_NAME_1)
-    }
-
-    private fun testTakingOverPermissionAndPermissionGroupDefinitions(
-        oldPermissionOwnerIsSystem: Boolean = false,
-        newPermissionOwnerIsSystem: Boolean = false,
-        newPermissionOwnerIsInstant: Boolean = false,
-        permissionGroupAlreadyExists: Boolean = true,
-        permissionAlreadyExists: Boolean = true,
-        type: Int = Permission.TYPE_MANIFEST,
-        isReconciled: Boolean = true,
-    ) {
-        val oldPermissionOwnerPackageState = mockPackageState(
-            APP_ID_1,
-            PACKAGE_NAME_1,
-            isSystem = oldPermissionOwnerIsSystem
-        )
-        addPackageState(oldPermissionOwnerPackageState)
-        if (permissionGroupAlreadyExists) {
-            addPermissionGroup(mockParsedPermissionGroup(PERMISSION_GROUP_NAME_0, PACKAGE_NAME_1))
-        }
-        if (permissionAlreadyExists) {
-            addPermission(
-                mockParsedPermission(PERMISSION_NAME_0, PACKAGE_NAME_1),
-                type = type,
-                isReconciled = isReconciled
-            )
-        }
-
-        mutateState {
-            val newPermissionOwnerPackageState = mockPackageState(
-                APP_ID_0,
-                mockSimpleAndroidPackage(),
-                isSystem = newPermissionOwnerIsSystem,
-                isInstantApp = newPermissionOwnerIsInstant
-            )
-            addPackageState(newPermissionOwnerPackageState, newState)
-            with(appIdPermissionPolicy) {
-                onPackageAdded(newPermissionOwnerPackageState)
-            }
-        }
-    }
-
-    @Test
-    fun testOnPackageAdded_permissionGroupChanged_getsRevoked() {
-        testPermissionChanged(
-            oldPermissionGroup = PERMISSION_GROUP_NAME_1,
-            newPermissionGroup = PERMISSION_GROUP_NAME_0
-        )
-
-        val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = 0
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that has a permission group change" +
-                " for a permission it defines, the actual permission flags $actualFlags" +
-                " should match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_protectionLevelChanged_getsRevoked() {
-        testPermissionChanged(newProtectionLevel = PermissionInfo.PROTECTION_INTERNAL)
-
-        val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = 0
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that has a protection level change" +
-                " for a permission it defines, the actual permission flags $actualFlags" +
-                " should match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    private fun testPermissionChanged(
-        oldPermissionGroup: String? = null,
-        newPermissionGroup: String? = null,
-        newProtectionLevel: Int = PermissionInfo.PROTECTION_DANGEROUS
-    ) {
-        val oldPermission = mockParsedPermission(
-            PERMISSION_NAME_0,
-            PACKAGE_NAME_0,
-            group = oldPermissionGroup,
-            protectionLevel = PermissionInfo.PROTECTION_DANGEROUS
-        )
-        val oldPackageState = mockPackageState(
-            APP_ID_0,
-            mockAndroidPackage(PACKAGE_NAME_0, permissions = listOf(oldPermission))
-        )
-        addPackageState(oldPackageState)
-        addPermission(oldPermission)
-        setPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0, PermissionFlags.RUNTIME_GRANTED)
-
-        mutateState {
-            val newPermission = mockParsedPermission(
-                PERMISSION_NAME_0,
-                PACKAGE_NAME_0,
-                group = newPermissionGroup,
-                protectionLevel = newProtectionLevel
-            )
-            val newPackageState = mockPackageState(
-                APP_ID_0,
-                mockAndroidPackage(PACKAGE_NAME_0, permissions = listOf(newPermission))
-            )
-            addPackageState(newPackageState, newState)
-            with(appIdPermissionPolicy) {
-                onPackageAdded(newPackageState)
-            }
-        }
-    }
-
-    @Test
-    fun testOnPackageAdded_permissionTreeNoLongerDeclared_getsDefinitionRemoved() {
-        testPermissionDeclaration {}
-
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that no longer defines a permission" +
-                " tree, the permission tree: $PERMISSION_NAME_0 in system state should be removed"
-        )
-            .that(getPermissionTree(PERMISSION_NAME_0))
-            .isNull()
-    }
-
-    @Test
-    fun testOnPackageAdded_permissionTreeByDisabledSystemPackage_remainsUnchanged() {
-        testPermissionDeclaration {
-            val disabledSystemPackageState = mockPackageState(APP_ID_0, mockSimpleAndroidPackage())
-            addDisabledSystemPackageState(disabledSystemPackageState)
-        }
-
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that no longer defines" +
-                " a permission tree while this permission tree is still defined by" +
-                " a disabled system package, the permission tree: $PERMISSION_NAME_0 in" +
-                " system state should not be removed"
-        )
-            .that(getPermissionTree(PERMISSION_TREE_NAME))
-            .isNotNull()
-    }
-
-    @Test
-    fun testOnPackageAdded_permissionNoLongerDeclared_getsDefinitionRemoved() {
-        testPermissionDeclaration {}
-
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that no longer defines a permission," +
-                " the permission: $PERMISSION_NAME_0 in system state should be removed"
-        )
-            .that(getPermission(PERMISSION_NAME_0))
-            .isNull()
-    }
-
-    @Test
-    fun testOnPackageAdded_permissionByDisabledSystemPackage_remainsUnchanged() {
-        testPermissionDeclaration {
-            val disabledSystemPackageState = mockPackageState(APP_ID_0, mockSimpleAndroidPackage())
-            addDisabledSystemPackageState(disabledSystemPackageState)
-        }
-
-        assertWithMessage(
-            "After onPackageAdded() is called for a disabled system package and it's updated apk" +
-                " no longer defines a permission, the permission: $PERMISSION_NAME_0 in" +
-                " system state should not be removed"
-        )
-            .that(getPermission(PERMISSION_NAME_0))
-            .isNotNull()
-    }
-
-    private fun testPermissionDeclaration(additionalSetup: () -> Unit) {
-        val oldPackageState = mockPackageState(APP_ID_0, mockSimpleAndroidPackage())
-        addPackageState(oldPackageState)
-        addPermission(defaultPermissionTree)
-        addPermission(defaultPermission)
-
-        additionalSetup()
-
-        mutateState {
-            val newPackageState = mockPackageState(APP_ID_0, mockAndroidPackage(PACKAGE_NAME_0))
-            addPackageState(newPackageState, newState)
-            with(appIdPermissionPolicy) {
-                onPackageAdded(newPackageState)
-            }
-        }
-    }
-
-    @Test
-    fun testOnPackageAdded_permissionsNoLongerRequested_getsFlagsRevoked() {
-        val parsedPermission = mockParsedPermission(
-            PERMISSION_NAME_0,
-            PACKAGE_NAME_0,
-            protectionLevel = PermissionInfo.PROTECTION_DANGEROUS
-        )
-        val oldPackageState = mockPackageState(
-            APP_ID_0,
-            mockAndroidPackage(
-                PACKAGE_NAME_0,
-                permissions = listOf(parsedPermission),
-                requestedPermissions = setOf(PERMISSION_NAME_0)
-            )
-        )
-        addPackageState(oldPackageState)
-        addPermission(parsedPermission)
-        setPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0, PermissionFlags.RUNTIME_GRANTED)
-
-        mutateState {
-            val newPackageState = mockPackageState(APP_ID_0, mockSimpleAndroidPackage())
-            addPackageState(newPackageState, newState)
-            with(appIdPermissionPolicy) {
-                onPackageAdded(newPackageState)
-            }
-        }
-
-        val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = 0
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that no longer requests a permission" +
-                " the actual permission flags $actualFlags should match the" +
-                " expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_storageAndMediaPermissionsDowngradingPastQ_getsRuntimeRevoked() {
-        testRevokePermissionsOnPackageUpdate(
-            PermissionFlags.RUNTIME_GRANTED,
-            newTargetSdkVersion = Build.VERSION_CODES.P
-        )
-
-        val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_READ_EXTERNAL_STORAGE)
-        val expectedNewFlags = 0
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that's downgrading past Q" +
-                " the actual permission flags $actualFlags should match the" +
-                " expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_storageAndMediaPermissionsNotDowngradingPastQ_remainsUnchanged() {
-        val oldFlags = PermissionFlags.RUNTIME_GRANTED
-        testRevokePermissionsOnPackageUpdate(
-            oldFlags,
-            oldTargetSdkVersion = Build.VERSION_CODES.P,
-            newTargetSdkVersion = Build.VERSION_CODES.P
-        )
-
-        val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_READ_EXTERNAL_STORAGE)
-        val expectedNewFlags = oldFlags
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that's not downgrading past Q" +
-                " the actual permission flags $actualFlags should match the" +
-                " expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_policyFixedDowngradingPastQ_remainsUnchanged() {
-        val oldFlags = PermissionFlags.RUNTIME_GRANTED and PermissionFlags.POLICY_FIXED
-        testRevokePermissionsOnPackageUpdate(oldFlags, newTargetSdkVersion = Build.VERSION_CODES.P)
-
-        val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_READ_EXTERNAL_STORAGE)
-        val expectedNewFlags = oldFlags
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that's downgrading past Q" +
-                " the actual permission flags with PermissionFlags.POLICY_FIXED $actualFlags" +
-                " should match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_newlyRequestingLegacyExternalStorage_getsRuntimeRevoked() {
-        testRevokePermissionsOnPackageUpdate(
-            PermissionFlags.RUNTIME_GRANTED,
-            oldTargetSdkVersion = Build.VERSION_CODES.P,
-            newTargetSdkVersion = Build.VERSION_CODES.P,
-            oldIsRequestLegacyExternalStorage = false
-        )
-
-        val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_READ_EXTERNAL_STORAGE)
-        val expectedNewFlags = 0
-        assertWithMessage(
-            "After onPackageAdded() is called for a package with" +
-                " newlyRequestingLegacyExternalStorage, the actual permission flags $actualFlags" +
-                " should match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_missingOldPackage_remainsUnchanged() {
-        val oldFlags = PermissionFlags.RUNTIME_GRANTED
-        testRevokePermissionsOnPackageUpdate(
-            oldFlags,
-            newTargetSdkVersion = Build.VERSION_CODES.P,
-            isOldPackageMissing = true
-        )
-
-        val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_READ_EXTERNAL_STORAGE)
-        val expectedNewFlags = oldFlags
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that's downgrading past Q" +
-                " and doesn't have the oldPackage, the actual permission flags $actualFlags" +
-                " should match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    private fun testRevokePermissionsOnPackageUpdate(
-        oldFlags: Int,
-        oldTargetSdkVersion: Int = Build.VERSION_CODES.UPSIDE_DOWN_CAKE,
-        newTargetSdkVersion: Int = Build.VERSION_CODES.UPSIDE_DOWN_CAKE,
-        oldIsRequestLegacyExternalStorage: Boolean = true,
-        newIsRequestLegacyExternalStorage: Boolean = true,
-        isOldPackageMissing: Boolean = false
-    ) {
-        val parsedPermission = mockParsedPermission(
-            PERMISSION_READ_EXTERNAL_STORAGE,
-            PACKAGE_NAME_0,
-            protectionLevel = PermissionInfo.PROTECTION_DANGEROUS
-        )
-        val oldPackageState = if (isOldPackageMissing) {
-            mockPackageState(APP_ID_0, PACKAGE_NAME_0)
-        } else {
-            mockPackageState(
-                APP_ID_0,
-                mockAndroidPackage(
-                    PACKAGE_NAME_0,
-                    targetSdkVersion = oldTargetSdkVersion,
-                    isRequestLegacyExternalStorage = oldIsRequestLegacyExternalStorage,
-                    requestedPermissions = setOf(PERMISSION_READ_EXTERNAL_STORAGE),
-                    permissions = listOf(parsedPermission)
-                )
-            )
-        }
-        addPackageState(oldPackageState)
-        addPermission(parsedPermission)
-        setPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_READ_EXTERNAL_STORAGE, oldFlags)
-
-        mutateState {
-            val newPackageState = mockPackageState(
-                APP_ID_0,
-                mockAndroidPackage(
-                    PACKAGE_NAME_0,
-                    targetSdkVersion = newTargetSdkVersion,
-                    isRequestLegacyExternalStorage = newIsRequestLegacyExternalStorage,
-                    requestedPermissions = setOf(PERMISSION_READ_EXTERNAL_STORAGE),
-                    permissions = listOf(parsedPermission)
-                )
-            )
-            addPackageState(newPackageState, newState)
-            with(appIdPermissionPolicy) {
-                onPackageAdded(newPackageState)
-            }
-        }
-    }
-
-    @Test
-    fun testOnPackageAdded_normalPermissionAlreadyGranted_remainsUnchanged() {
-        val oldFlags = PermissionFlags.INSTALL_GRANTED or PermissionFlags.INSTALL_REVOKED
-        testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_NORMAL) {}
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = oldFlags
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests a normal permission" +
-                " with an existing INSTALL_GRANTED flag, the actual permission flags $actualFlags" +
-                " should match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_normalPermissionNotInstallRevoked_getsGranted() {
-        val oldFlags = 0
-        testEvaluatePermissionState(
-            oldFlags,
-            PermissionInfo.PROTECTION_NORMAL,
-            isNewInstall = true
-        ) {}
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = PermissionFlags.INSTALL_GRANTED
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests a normal permission" +
-                " with no existing flags, the actual permission flags $actualFlags" +
-                " should match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_normalPermissionRequestedByInstalledPackage_getsGranted() {
-        val oldFlags = PermissionFlags.INSTALL_REVOKED
-        testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_NORMAL) {}
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = PermissionFlags.INSTALL_GRANTED
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests a normal permission" +
-                " with the INSTALL_REVOKED flag, the actual permission flags $actualFlags" +
-                " should match the expected flags $expectedNewFlags since it's a new install"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    /**
-     * We setup a permission protection level change from SIGNATURE to NORMAL in order to make
-     * the permission a "changed permission" in order to test evaluatePermissionState() called by
-     * evaluatePermissionStateForAllPackages(). This makes the requestingPackageState not the
-     * installedPackageState so that we can test whether requesting by system package will give us
-     * the expected permission flags.
-     *
-     * Besides, this also helps us test evaluatePermissionStateForAllPackages(). Since both
-     * evaluatePermissionStateForAllPackages() and evaluateAllPermissionStatesForPackage() call
-     * evaluatePermissionState() in their implementations, we use these tests as the only tests
-     * that test evaluatePermissionStateForAllPackages()
-     */
-    @Test
-    fun testOnPackageAdded_normalPermissionRequestedBySystemPackage_getsGranted() {
-        testEvaluateNormalPermissionStateWithPermissionChanges(requestingPackageIsSystem = true)
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = PermissionFlags.INSTALL_GRANTED
-        assertWithMessage(
-            "After onPackageAdded() is called for a system package that requests a normal" +
-                " permission with INSTALL_REVOKED flag, the actual permission flags $actualFlags" +
-                " should match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_normalCompatibilityPermission_getsGranted() {
-        testEvaluateNormalPermissionStateWithPermissionChanges(
-            permissionName = PERMISSION_POST_NOTIFICATIONS,
-            requestingPackageTargetSdkVersion = Build.VERSION_CODES.S
-        )
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_POST_NOTIFICATIONS)
-        val expectedNewFlags = PermissionFlags.INSTALL_GRANTED
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests a normal compatibility" +
-                " permission with INSTALL_REVOKED flag, the actual permission flags $actualFlags" +
-                " should match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_normalPermissionPreviouslyRevoked_getsInstallRevoked() {
-        testEvaluateNormalPermissionStateWithPermissionChanges()
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = PermissionFlags.INSTALL_REVOKED
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests a normal" +
-                " permission with INSTALL_REVOKED flag, the actual permission flags $actualFlags" +
-                " should match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    private fun testEvaluateNormalPermissionStateWithPermissionChanges(
-        permissionName: String = PERMISSION_NAME_0,
-        requestingPackageTargetSdkVersion: Int = Build.VERSION_CODES.UPSIDE_DOWN_CAKE,
-        requestingPackageIsSystem: Boolean = false
-    ) {
-        val oldParsedPermission = mockParsedPermission(
-            permissionName,
-            PACKAGE_NAME_0,
-            protectionLevel = PermissionInfo.PROTECTION_SIGNATURE
-        )
-        val oldPermissionOwnerPackageState = mockPackageState(
-            APP_ID_0,
-            mockAndroidPackage(PACKAGE_NAME_0, permissions = listOf(oldParsedPermission))
-        )
-        val requestingPackageState = mockPackageState(
-            APP_ID_1,
-            mockAndroidPackage(
-                PACKAGE_NAME_1,
-                requestedPermissions = setOf(permissionName),
-                targetSdkVersion = requestingPackageTargetSdkVersion
-            ),
-            isSystem = requestingPackageIsSystem,
-        )
-        addPackageState(oldPermissionOwnerPackageState)
-        addPackageState(requestingPackageState)
-        addPermission(oldParsedPermission)
-        val oldFlags = PermissionFlags.INSTALL_REVOKED
-        setPermissionFlags(APP_ID_1, USER_ID_0, permissionName, oldFlags)
-
-        mutateState {
-            val newPermissionOwnerPackageState = mockPackageState(
-                APP_ID_0,
-                mockAndroidPackage(
-                    PACKAGE_NAME_0,
-                    permissions = listOf(mockParsedPermission(permissionName, PACKAGE_NAME_0))
-                )
-            )
-            addPackageState(newPermissionOwnerPackageState, newState)
-            with(appIdPermissionPolicy) {
-                onPackageAdded(newPermissionOwnerPackageState)
-            }
-        }
-    }
-
-    @Test
-    fun testOnPackageAdded_normalAppOpPermission_getsRoleAndUserSetFlagsPreserved() {
-        val oldFlags = PermissionFlags.ROLE or PermissionFlags.USER_SET
-        testEvaluatePermissionState(
-            oldFlags,
-            PermissionInfo.PROTECTION_NORMAL or PermissionInfo.PROTECTION_FLAG_APPOP
-        ) {}
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = PermissionFlags.INSTALL_GRANTED or oldFlags
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests a normal app op" +
-                " permission with existing ROLE and USER_SET flags, the actual permission flags" +
-                " $actualFlags should match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_internalPermissionWasGrantedWithMissingPackage_getsProtectionGranted() {
-        val oldFlags = PermissionFlags.PROTECTION_GRANTED
-        testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_INTERNAL) {
-            val packageStateWithMissingPackage = mockPackageState(APP_ID_1, MISSING_ANDROID_PACKAGE)
-            addPackageState(packageStateWithMissingPackage)
-        }
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = oldFlags
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests an internal permission" +
-                " with missing android package and $oldFlags flag, the actual permission flags" +
-                " $actualFlags should match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_internalAppOpPermission_getsRoleAndUserSetFlagsPreserved() {
-        val oldFlags = PermissionFlags.PROTECTION_GRANTED or PermissionFlags.ROLE or
-            PermissionFlags.USER_SET
-        testEvaluatePermissionState(
-            oldFlags,
-            PermissionInfo.PROTECTION_INTERNAL or PermissionInfo.PROTECTION_FLAG_APPOP
-        ) {
-            val packageStateWithMissingPackage = mockPackageState(APP_ID_1, MISSING_ANDROID_PACKAGE)
-            addPackageState(packageStateWithMissingPackage)
-        }
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = oldFlags
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests an internal permission" +
-                " with missing android package and $oldFlags flag and the permission isAppOp," +
-                " the actual permission flags $actualFlags should match the expected" +
-                " flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_internalDevelopmentPermission_getsRuntimeGrantedPreserved() {
-        val oldFlags = PermissionFlags.PROTECTION_GRANTED or PermissionFlags.RUNTIME_GRANTED
-        testEvaluatePermissionState(
-            oldFlags,
-            PermissionInfo.PROTECTION_INTERNAL or PermissionInfo.PROTECTION_FLAG_DEVELOPMENT
-        ) {
-            val packageStateWithMissingPackage = mockPackageState(APP_ID_1, MISSING_ANDROID_PACKAGE)
-            addPackageState(packageStateWithMissingPackage)
-        }
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = oldFlags
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests an internal permission" +
-                " with missing android package and $oldFlags flag and permission isDevelopment," +
-                " the actual permission flags $actualFlags should match the expected" +
-                " flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_internalRolePermission_getsRoleAndRuntimeGrantedPreserved() {
-        val oldFlags = PermissionFlags.PROTECTION_GRANTED or PermissionFlags.ROLE or
-            PermissionFlags.RUNTIME_GRANTED
-        testEvaluatePermissionState(
-            oldFlags,
-            PermissionInfo.PROTECTION_INTERNAL or PermissionInfo.PROTECTION_FLAG_ROLE
-        ) {
-            val packageStateWithMissingPackage = mockPackageState(APP_ID_1, MISSING_ANDROID_PACKAGE)
-            addPackageState(packageStateWithMissingPackage)
-        }
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = oldFlags
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests an internal permission" +
-                " with missing android package and $oldFlags flag and the permission isRole," +
-                " the actual permission flags $actualFlags should match the expected" +
-                " flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_signaturePrivilegedPermissionNotAllowlisted_isNotGranted() {
-        val oldFlags = 0
-        testEvaluatePermissionState(
-            oldFlags,
-            PermissionInfo.PROTECTION_SIGNATURE or PermissionInfo.PROTECTION_FLAG_PRIVILEGED,
-            isInstalledPackageSystem = true,
-            isInstalledPackagePrivileged = true,
-            isInstalledPackageProduct = true,
-            // To mock the return value of shouldGrantPrivilegedOrOemPermission()
-            isInstalledPackageVendor = true,
-            isNewInstall = true
-        ) {
-            val platformPackage = mockPackageState(
-                PLATFORM_APP_ID,
-                mockAndroidPackage(PLATFORM_PACKAGE_NAME)
-            )
-            setupAllowlist(PACKAGE_NAME_1, false)
-            addPackageState(platformPackage)
-        }
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = oldFlags
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests a signature privileged" +
-                " permission that's not allowlisted, the actual permission" +
-                " flags $actualFlags should match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_nonPrivilegedPermissionShouldGrantBySignature_getsProtectionGranted() {
-        val oldFlags = 0
-        testEvaluatePermissionState(
-            oldFlags,
-            PermissionInfo.PROTECTION_SIGNATURE,
-            isInstalledPackageSystem = true,
-            isInstalledPackagePrivileged = true,
-            isInstalledPackageProduct = true,
-            isInstalledPackageSignatureMatching = true,
-            isInstalledPackageVendor = true,
-            isNewInstall = true
-        ) {
-            val platformPackage = mockPackageState(
-                PLATFORM_APP_ID,
-                mockAndroidPackage(PLATFORM_PACKAGE_NAME, isSignatureMatching = true)
-            )
-            setupAllowlist(PACKAGE_NAME_1, false)
-            addPackageState(platformPackage)
-        }
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = PermissionFlags.PROTECTION_GRANTED
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests a signature" +
-                " non-privileged permission, the actual permission" +
-                " flags $actualFlags should match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_privilegedAllowlistPermissionShouldGrantByProtectionFlags_getsGranted() {
-        val oldFlags = 0
-        testEvaluatePermissionState(
-            oldFlags,
-            PermissionInfo.PROTECTION_SIGNATURE or PermissionInfo.PROTECTION_FLAG_PRIVILEGED,
-            isInstalledPackageSystem = true,
-            isInstalledPackagePrivileged = true,
-            isInstalledPackageProduct = true,
-            isNewInstall = true
-        ) {
-            val platformPackage = mockPackageState(
-                PLATFORM_APP_ID,
-                mockAndroidPackage(PLATFORM_PACKAGE_NAME)
-            )
-            setupAllowlist(PACKAGE_NAME_1, true)
-            addPackageState(platformPackage)
-        }
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = PermissionFlags.PROTECTION_GRANTED
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests a signature privileged" +
-                " permission that's allowlisted and should grant by protection flags, the actual" +
-                " permission flags $actualFlags should match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    private fun setupAllowlist(
-        packageName: String,
-        allowlistState: Boolean,
-        state: MutableAccessState = oldState
-    ) {
-        state.mutateExternalState().setPrivilegedPermissionAllowlistPackages(
-            MutableIndexedListSet<String>().apply { add(packageName) }
-        )
-        val mockAllowlist = mock<PermissionAllowlist> {
-            whenever(
-                getProductPrivilegedAppAllowlistState(packageName, PERMISSION_NAME_0)
-            ).thenReturn(allowlistState)
-        }
-        state.mutateExternalState().setPermissionAllowlist(mockAllowlist)
-    }
-
-    @Test
-    fun testOnPackageAdded_nonRuntimeFlagsOnRuntimePermissions_getsCleared() {
-        val oldFlags = PermissionFlags.INSTALL_GRANTED or PermissionFlags.PREGRANT or
-            PermissionFlags.RUNTIME_GRANTED
-        testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {}
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = PermissionFlags.PREGRANT or PermissionFlags.RUNTIME_GRANTED
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests a runtime permission" +
-                " with existing $oldFlags flags, the actual permission flags $actualFlags should" +
-                " match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_newPermissionsForPreM_requiresUserReview() {
-        val oldFlags = 0
-        testEvaluatePermissionState(
-            oldFlags,
-            PermissionInfo.PROTECTION_DANGEROUS,
-            installedPackageTargetSdkVersion = Build.VERSION_CODES.LOLLIPOP,
-            isNewInstall = true
-        ) {}
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = PermissionFlags.LEGACY_GRANTED or PermissionFlags.IMPLICIT
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests a runtime permission" +
-                " with no existing flags in pre M, actual permission flags $actualFlags should" +
-                " match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_legacyOrImplicitGrantedPermissionPreviouslyRevoked_getsAppOpRevoked() {
-        val oldFlags = PermissionFlags.USER_FIXED
-        testEvaluatePermissionState(
-            oldFlags,
-            PermissionInfo.PROTECTION_DANGEROUS,
-            installedPackageTargetSdkVersion = Build.VERSION_CODES.LOLLIPOP
-        ) {
-            setPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0, oldFlags)
-        }
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = PermissionFlags.LEGACY_GRANTED or PermissionFlags.USER_FIXED or
-            PermissionFlags.APP_OP_REVOKED
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests a runtime permission" +
-                " that should be LEGACY_GRANTED or IMPLICIT_GRANTED that was previously revoked," +
-                " the actual permission flags $actualFlags should" +
-                " match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_legacyGrantedPermissionsForPostM_userReviewRequirementRemoved() {
-        val oldFlags = PermissionFlags.LEGACY_GRANTED or PermissionFlags.IMPLICIT
-        testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {}
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = 0
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests a runtime permission" +
-                " that used to require user review, the user review requirement should be removed" +
-                " if it's upgraded to post M. The actual permission flags $actualFlags should" +
-                " match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_legacyGrantedPermissionsAlreadyReviewedForPostM_getsGranted() {
-        val oldFlags = PermissionFlags.LEGACY_GRANTED
-        testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {}
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = PermissionFlags.RUNTIME_GRANTED
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests a runtime permission" +
-                " that was already reviewed by the user, the permission should be RUNTIME_GRANTED" +
-                " if it's upgraded to post M. The actual permission flags $actualFlags should" +
-                " match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_leanbackNotificationPermissionsForPostM_getsImplicitGranted() {
-        val oldFlags = 0
-        testEvaluatePermissionState(
-            oldFlags,
-            PermissionInfo.PROTECTION_DANGEROUS,
-            permissionName = PERMISSION_POST_NOTIFICATIONS,
-            isNewInstall = true
-        ) {
-            oldState.mutateExternalState().setLeanback(true)
-        }
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_POST_NOTIFICATIONS)
-        val expectedNewFlags = PermissionFlags.IMPLICIT_GRANTED
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests a runtime notification" +
-                " permission when isLeanback, the actual permission flags $actualFlags should" +
-                " match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_implicitSourceFromNonRuntime_getsImplicitGranted() {
-        val oldFlags = 0
-        testEvaluatePermissionState(
-            oldFlags,
-            PermissionInfo.PROTECTION_DANGEROUS,
-            implicitPermissions = setOf(PERMISSION_NAME_0),
-            isNewInstall = true
-        ) {
-            oldState.mutateExternalState().setImplicitToSourcePermissions(
-                MutableIndexedMap<String, IndexedListSet<String>>().apply {
-                    put(PERMISSION_NAME_0, MutableIndexedListSet<String>().apply {
-                        add(PERMISSION_NAME_1)
-                    })
-                }
-            )
-            addPermission(mockParsedPermission(PERMISSION_NAME_1, PACKAGE_NAME_0))
-        }
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = PermissionFlags.IMPLICIT_GRANTED or PermissionFlags.IMPLICIT
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests a runtime implicit" +
-                " permission that's source from a non-runtime permission, the actual permission" +
-                " flags $actualFlags should match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    /**
-     * For a legacy granted or implicit permission during the app upgrade, when the permission
-     * should no longer be legacy or implicit granted, we want to remove the APP_OP_REVOKED flag
-     * so that the app can request the permission.
-     */
-    @Test
-    fun testOnPackageAdded_noLongerLegacyOrImplicitGranted_canBeRequested() {
-        val oldFlags = PermissionFlags.LEGACY_GRANTED or PermissionFlags.APP_OP_REVOKED or
-            PermissionFlags.RUNTIME_GRANTED
-        testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {}
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = 0
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests a runtime permission" +
-                " that is no longer LEGACY_GRANTED or IMPLICIT_GRANTED, the actual permission" +
-                " flags $actualFlags should match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_noLongerImplicitPermissions_getsRuntimeAndImplicitFlagsRemoved() {
-        val oldFlags = PermissionFlags.IMPLICIT or PermissionFlags.RUNTIME_GRANTED or
-            PermissionFlags.USER_SET or PermissionFlags.USER_FIXED
-        testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {}
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = 0
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests a runtime permission" +
-                " that is no longer implicit and we shouldn't retain as nearby device" +
-                " permissions, the actual permission flags $actualFlags should match the expected" +
-                " flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_noLongerImplicitNearbyPermissionsWasGranted_getsRuntimeGranted() {
-        val oldFlags = PermissionFlags.IMPLICIT_GRANTED or PermissionFlags.IMPLICIT
-        testEvaluatePermissionState(
-            oldFlags,
-            PermissionInfo.PROTECTION_DANGEROUS,
-            permissionName = PERMISSION_BLUETOOTH_CONNECT,
-            requestedPermissions = setOf(
-                PERMISSION_BLUETOOTH_CONNECT,
-                PERMISSION_ACCESS_BACKGROUND_LOCATION
-            )
-        ) {
-            setPermissionFlags(
-                APP_ID_1,
-                USER_ID_0,
-                PERMISSION_ACCESS_BACKGROUND_LOCATION,
-                PermissionFlags.RUNTIME_GRANTED
-            )
-        }
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_BLUETOOTH_CONNECT)
-        val expectedNewFlags = PermissionFlags.RUNTIME_GRANTED
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests a runtime nearby device" +
-                " permission that was granted by implicit, the actual permission flags" +
-                " $actualFlags should match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_noLongerImplicitSystemOrPolicyFixedWasGranted_getsRuntimeGranted() {
-        val oldFlags = PermissionFlags.IMPLICIT_GRANTED or PermissionFlags.IMPLICIT or
-            PermissionFlags.SYSTEM_FIXED
-        testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {}
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = PermissionFlags.RUNTIME_GRANTED or PermissionFlags.SYSTEM_FIXED
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests a runtime permission" +
-                " that was granted and is no longer implicit and is SYSTEM_FIXED or POLICY_FIXED," +
-                " the actual permission flags $actualFlags should match the expected" +
-                " flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_restrictedPermissionsNotExempt_getsRestrictionFlags() {
-        val oldFlags = PermissionFlags.RESTRICTION_REVOKED
-        testEvaluatePermissionState(
-            oldFlags,
-            PermissionInfo.PROTECTION_DANGEROUS,
-            permissionInfoFlags = PermissionInfo.FLAG_HARD_RESTRICTED
-        ) {}
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = oldFlags
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests a runtime hard" +
-                " restricted permission that is not exempted, the actual permission flags" +
-                " $actualFlags should match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_restrictedPermissionsIsExempted_clearsRestrictionFlags() {
-        val oldFlags = 0
-        testEvaluatePermissionState(
-            oldFlags,
-            PermissionInfo.PROTECTION_DANGEROUS,
-            permissionInfoFlags = PermissionInfo.FLAG_SOFT_RESTRICTED
-        ) {}
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = PermissionFlags.UPGRADE_EXEMPT
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests a runtime soft" +
-                " restricted permission that is exempted, the actual permission flags" +
-                " $actualFlags should match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_runtimeExistingImplicitPermissions_sourceFlagsNotInherited() {
-        val oldImplicitPermissionFlags = PermissionFlags.USER_FIXED
-        testInheritImplicitPermissionStates(
-            implicitPermissionFlags = oldImplicitPermissionFlags,
-            isNewInstallAndNewPermission = false
-        )
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = oldImplicitPermissionFlags or PermissionFlags.IMPLICIT_GRANTED or
-            PermissionFlags.APP_OP_REVOKED
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests a permission that is" +
-                " implicit, existing and runtime, it should not inherit the runtime flags from" +
-                " the source permission. Hence the actual permission flags $actualFlags should" +
-                " match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_nonRuntimeNewImplicitPermissions_sourceFlagsNotInherited() {
-        testInheritImplicitPermissionStates(
-            implicitPermissionProtectionLevel = PermissionInfo.PROTECTION_NORMAL
-        )
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = PermissionFlags.INSTALL_GRANTED
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests a permission that is" +
-                " implicit, new and non-runtime, it should not inherit the runtime flags from" +
-                " the source permission. Hence the actual permission flags $actualFlags should" +
-                " match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_runtimeNewImplicitPermissions_sourceFlagsInherited() {
-        val sourceRuntimeFlags = PermissionFlags.RUNTIME_GRANTED or PermissionFlags.USER_SET
-        testInheritImplicitPermissionStates(sourceRuntimeFlags = sourceRuntimeFlags)
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = sourceRuntimeFlags or PermissionFlags.IMPLICIT_GRANTED or
-            PermissionFlags.IMPLICIT
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests a permission that is" +
-                " implicit, new and runtime, it should inherit the runtime flags from" +
-                " the source permission. Hence the actual permission flags $actualFlags should" +
-                " match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    @Test
-    fun testOnPackageAdded_grantingNewFromRevokeImplicitPermissions_onlySourceFlagsInherited() {
-        val sourceRuntimeFlags = PermissionFlags.RUNTIME_GRANTED or PermissionFlags.USER_SET
-        testInheritImplicitPermissionStates(
-            implicitPermissionFlags = PermissionFlags.POLICY_FIXED,
-            sourceRuntimeFlags = sourceRuntimeFlags,
-            isAnySourcePermissionNonRuntime = false
-        )
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
-        val expectedNewFlags = sourceRuntimeFlags or PermissionFlags.IMPLICIT
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests a permission that is" +
-                " implicit, existing, runtime and revoked, it should only inherit runtime flags" +
-                " from source permission. Hence the actual permission flags $actualFlags should" +
-                " match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    /**
-     * If it's a media implicit permission (one of RETAIN_IMPLICIT_FLAGS_PERMISSIONS), we want to
-     * remove the IMPLICIT flag so that they will be granted when they are no longer implicit.
-     * (instead of revoking it)
-     */
-    @Test
-    fun testOnPackageAdded_mediaImplicitPermissions_getsImplicitFlagRemoved() {
-        val sourceRuntimeFlags = PermissionFlags.RUNTIME_GRANTED or PermissionFlags.USER_SET
-        testInheritImplicitPermissionStates(
-            implicitPermissionName = PERMISSION_ACCESS_MEDIA_LOCATION,
-            sourceRuntimeFlags = sourceRuntimeFlags
-        )
-
-        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_ACCESS_MEDIA_LOCATION)
-        val expectedNewFlags = sourceRuntimeFlags or PermissionFlags.IMPLICIT_GRANTED
-        assertWithMessage(
-            "After onPackageAdded() is called for a package that requests a media permission that" +
-                " is implicit, new and runtime, it should inherit the runtime flags from" +
-                " the source permission and have the IMPLICIT flag removed. Hence the actual" +
-                " permission flags $actualFlags should match the expected flags $expectedNewFlags"
-        )
-            .that(actualFlags)
-            .isEqualTo(expectedNewFlags)
-    }
-
-    private fun testInheritImplicitPermissionStates(
-        implicitPermissionName: String = PERMISSION_NAME_0,
-        implicitPermissionFlags: Int = 0,
-        implicitPermissionProtectionLevel: Int = PermissionInfo.PROTECTION_DANGEROUS,
-        sourceRuntimeFlags: Int = PermissionFlags.RUNTIME_GRANTED or PermissionFlags.USER_SET,
-        isAnySourcePermissionNonRuntime: Boolean = true,
-        isNewInstallAndNewPermission: Boolean = true
-    ) {
-        val implicitPermission = mockParsedPermission(
-            implicitPermissionName,
-            PACKAGE_NAME_0,
-            protectionLevel = implicitPermissionProtectionLevel,
-        )
-        // For source from non-runtime in order to grant by implicit
-        val sourcePermission1 = mockParsedPermission(
-            PERMISSION_NAME_1,
-            PACKAGE_NAME_0,
-            protectionLevel = if (isAnySourcePermissionNonRuntime) {
-                PermissionInfo.PROTECTION_NORMAL
-            } else {
-                PermissionInfo.PROTECTION_DANGEROUS
-            }
-        )
-        // For inheriting runtime flags
-        val sourcePermission2 = mockParsedPermission(
-            PERMISSION_NAME_2,
-            PACKAGE_NAME_0,
-            protectionLevel = PermissionInfo.PROTECTION_DANGEROUS,
-        )
-        val permissionOwnerPackageState = mockPackageState(
-            APP_ID_0,
-            mockAndroidPackage(
-                PACKAGE_NAME_0,
-                permissions = listOf(implicitPermission, sourcePermission1, sourcePermission2)
-            )
-        )
-        val installedPackageState = mockPackageState(
-            APP_ID_1,
-            mockAndroidPackage(
-                PACKAGE_NAME_1,
-                requestedPermissions = setOf(
-                    implicitPermissionName,
-                    PERMISSION_NAME_1,
-                    PERMISSION_NAME_2
-                ),
-                implicitPermissions = setOf(implicitPermissionName)
-            )
-        )
-        oldState.mutateExternalState().setImplicitToSourcePermissions(
-            MutableIndexedMap<String, IndexedListSet<String>>().apply {
-                put(implicitPermissionName, MutableIndexedListSet<String>().apply {
-                    add(PERMISSION_NAME_1)
-                    add(PERMISSION_NAME_2)
-                })
-            }
-        )
-        addPackageState(permissionOwnerPackageState)
-        addPermission(implicitPermission)
-        addPermission(sourcePermission1)
-        addPermission(sourcePermission2)
-        if (!isNewInstallAndNewPermission) {
-            addPackageState(installedPackageState)
-            setPermissionFlags(APP_ID_1, USER_ID_0, implicitPermissionName, implicitPermissionFlags)
-        }
-        setPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_2, sourceRuntimeFlags)
-
-        mutateState {
-            if (isNewInstallAndNewPermission) {
-                addPackageState(installedPackageState)
-                setPermissionFlags(
-                    APP_ID_1,
-                    USER_ID_0,
-                    implicitPermissionName,
-                    implicitPermissionFlags,
-                    newState
-                )
-            }
-            with(appIdPermissionPolicy) {
-                onPackageAdded(installedPackageState)
-            }
-        }
-    }
-
-    /**
-     * Setup simple package states for testing evaluatePermissionState().
-     * permissionOwnerPackageState is definer of permissionName with APP_ID_0.
-     * installedPackageState is the installed package that requests permissionName with APP_ID_1.
-     *
-     * @param oldFlags the existing permission flags for APP_ID_1, USER_ID_0, permissionName
-     * @param protectionLevel the protectionLevel for the permission
-     * @param permissionName the name of the permission (1) being defined (2) of the oldFlags, and
-     *                       (3) requested by installedPackageState
-     * @param requestedPermissions the permissions requested by installedPackageState
-     * @param implicitPermissions the implicit permissions of installedPackageState
-     * @param permissionInfoFlags the flags for the permission itself
-     * @param isInstalledPackageSystem whether installedPackageState is a system package
-     *
-     * @return installedPackageState
-     */
-    fun testEvaluatePermissionState(
-        oldFlags: Int,
-        protectionLevel: Int,
-        permissionName: String = PERMISSION_NAME_0,
-        requestedPermissions: Set<String> = setOf(permissionName),
-        implicitPermissions: Set<String> = emptySet(),
-        permissionInfoFlags: Int = 0,
-        isInstalledPackageSystem: Boolean = false,
-        isInstalledPackagePrivileged: Boolean = false,
-        isInstalledPackageProduct: Boolean = false,
-        isInstalledPackageSignatureMatching: Boolean = false,
-        isInstalledPackageVendor: Boolean = false,
-        installedPackageTargetSdkVersion: Int = Build.VERSION_CODES.UPSIDE_DOWN_CAKE,
-        isNewInstall: Boolean = false,
-        additionalSetup: () -> Unit
-    ) {
-        val parsedPermission = mockParsedPermission(
-            permissionName,
-            PACKAGE_NAME_0,
-            protectionLevel = protectionLevel,
-            flags = permissionInfoFlags
-        )
-        val permissionOwnerPackageState = mockPackageState(
-            APP_ID_0,
-            mockAndroidPackage(PACKAGE_NAME_0, permissions = listOf(parsedPermission))
-        )
-        val installedPackageState = mockPackageState(
-            APP_ID_1,
-            mockAndroidPackage(
-                PACKAGE_NAME_1,
-                requestedPermissions = requestedPermissions,
-                implicitPermissions = implicitPermissions,
-                targetSdkVersion = installedPackageTargetSdkVersion,
-                isSignatureMatching = isInstalledPackageSignatureMatching
-            ),
-            isSystem = isInstalledPackageSystem,
-            isPrivileged = isInstalledPackagePrivileged,
-            isProduct = isInstalledPackageProduct,
-            isVendor = isInstalledPackageVendor
-        )
-        addPackageState(permissionOwnerPackageState)
-        if (!isNewInstall) {
-            addPackageState(installedPackageState)
-            setPermissionFlags(APP_ID_1, USER_ID_0, permissionName, oldFlags)
-        }
-        addPermission(parsedPermission)
-
-        additionalSetup()
-
-        mutateState {
-            if (isNewInstall) {
-                addPackageState(installedPackageState, newState)
-                setPermissionFlags(APP_ID_1, USER_ID_0, permissionName, oldFlags, newState)
-            }
-            with(appIdPermissionPolicy) {
-                onPackageAdded(installedPackageState)
-            }
-        }
-    }
-
-    /**
-     * Mock an AndroidPackage with PACKAGE_NAME_0, PERMISSION_NAME_0 and PERMISSION_GROUP_NAME_0
-     */
-    private fun mockSimpleAndroidPackage(): AndroidPackage =
-        mockAndroidPackage(
-            PACKAGE_NAME_0,
-            permissionGroups = listOf(defaultPermissionGroup),
-            permissions = listOf(defaultPermissionTree, defaultPermission)
-        )
-
-    private inline fun mutateState(action: MutateStateScope.() -> Unit) {
-        newState = oldState.toMutable()
-        MutateStateScope(oldState, newState).action()
-    }
-
-    private fun mockPackageState(
-        appId: Int,
-        packageName: String,
-        isSystem: Boolean = false,
-    ): PackageState =
-        mock {
-            whenever(this.appId).thenReturn(appId)
-            whenever(this.packageName).thenReturn(packageName)
-            whenever(androidPackage).thenReturn(null)
-            whenever(this.isSystem).thenReturn(isSystem)
-        }
-
-    private fun mockPackageState(
-        appId: Int,
-        androidPackage: AndroidPackage,
-        isSystem: Boolean = false,
-        isPrivileged: Boolean = false,
-        isProduct: Boolean = false,
-        isInstantApp: Boolean = false,
-        isVendor: Boolean = false
-    ): PackageState =
-        mock {
-            whenever(this.appId).thenReturn(appId)
-            whenever(this.androidPackage).thenReturn(androidPackage)
-            val packageName = androidPackage.packageName
-            whenever(this.packageName).thenReturn(packageName)
-            whenever(this.isSystem).thenReturn(isSystem)
-            whenever(this.isPrivileged).thenReturn(isPrivileged)
-            whenever(this.isProduct).thenReturn(isProduct)
-            whenever(this.isVendor).thenReturn(isVendor)
-            val userStates = SparseArray<PackageUserState>().apply {
-                put(USER_ID_0, mock { whenever(this.isInstantApp).thenReturn(isInstantApp) })
-            }
-            whenever(this.userStates).thenReturn(userStates)
-        }
-
-    private fun mockAndroidPackage(
-        packageName: String,
-        targetSdkVersion: Int = Build.VERSION_CODES.UPSIDE_DOWN_CAKE,
-        isRequestLegacyExternalStorage: Boolean = false,
-        adoptPermissions: List<String> = emptyList(),
-        implicitPermissions: Set<String> = emptySet(),
-        requestedPermissions: Set<String> = emptySet(),
-        permissionGroups: List<ParsedPermissionGroup> = emptyList(),
-        permissions: List<ParsedPermission> = emptyList(),
-        isSignatureMatching: Boolean = false
-    ): AndroidPackage =
-        mock {
-            whenever(this.packageName).thenReturn(packageName)
-            whenever(this.targetSdkVersion).thenReturn(targetSdkVersion)
-            whenever(this.isRequestLegacyExternalStorage).thenReturn(isRequestLegacyExternalStorage)
-            whenever(this.adoptPermissions).thenReturn(adoptPermissions)
-            whenever(this.implicitPermissions).thenReturn(implicitPermissions)
-            whenever(this.requestedPermissions).thenReturn(requestedPermissions)
-            whenever(this.permissionGroups).thenReturn(permissionGroups)
-            whenever(this.permissions).thenReturn(permissions)
-            val signingDetails = mock<SigningDetails> {
-                whenever(
-                    hasCommonSignerWithCapability(any(), any())
-                ).thenReturn(isSignatureMatching)
-                whenever(hasAncestorOrSelf(any())).thenReturn(isSignatureMatching)
-                whenever(
-                    checkCapability(any<SigningDetails>(), any())
-                ).thenReturn(isSignatureMatching)
-            }
-            whenever(this.signingDetails).thenReturn(signingDetails)
-        }
-
-    private fun mockParsedPermission(
-        permissionName: String,
-        packageName: String,
-        backgroundPermission: String? = null,
-        group: String? = null,
-        protectionLevel: Int = PermissionInfo.PROTECTION_NORMAL,
-        flags: Int = 0,
-        isTree: Boolean = false
-    ): ParsedPermission =
-        mock {
-            whenever(name).thenReturn(permissionName)
-            whenever(this.packageName).thenReturn(packageName)
-            whenever(metaData).thenReturn(Bundle())
-            whenever(this.backgroundPermission).thenReturn(backgroundPermission)
-            whenever(this.group).thenReturn(group)
-            whenever(this.protectionLevel).thenReturn(protectionLevel)
-            whenever(this.flags).thenReturn(flags)
-            whenever(this.isTree).thenReturn(isTree)
-        }
-
-    private fun mockParsedPermissionGroup(
-        permissionGroupName: String,
-        packageName: String,
-    ): ParsedPermissionGroup =
-        mock {
-            whenever(name).thenReturn(permissionGroupName)
-            whenever(this.packageName).thenReturn(packageName)
-            whenever(metaData).thenReturn(Bundle())
-        }
-
-    private fun addPackageState(packageState: PackageState, state: MutableAccessState = oldState) {
-        state.mutateExternalState().apply {
-            setPackageStates(
-                packageStates.toMutableMap().apply {
-                    put(packageState.packageName, packageState)
-                }
-            )
-            mutateAppIdPackageNames().mutateOrPut(packageState.appId) { MutableIndexedListSet() }
-                .add(packageState.packageName)
-        }
-    }
-
-    private fun addDisabledSystemPackageState(
-        packageState: PackageState,
-        state: MutableAccessState = oldState
-    ) = state.mutateExternalState().apply {
-        (disabledSystemPackageStates as ArrayMap)[packageState.packageName] = packageState
-    }
-
-    private fun addPermission(
-        parsedPermission: ParsedPermission,
-        type: Int = Permission.TYPE_MANIFEST,
-        isReconciled: Boolean = true,
-        state: MutableAccessState = oldState
-    ) {
-        val permissionInfo = PackageInfoUtils.generatePermissionInfo(
-            parsedPermission,
-            PackageManager.GET_META_DATA.toLong()
-        )!!
-        val appId = state.externalState.packageStates[permissionInfo.packageName]!!.appId
-        val permission = Permission(permissionInfo, isReconciled, type, appId)
-        if (parsedPermission.isTree) {
-            state.mutateSystemState().mutatePermissionTrees()[permission.name] = permission
-        } else {
-            state.mutateSystemState().mutatePermissions()[permission.name] = permission
-        }
-    }
-
-    private fun addPermissionGroup(
-        parsedPermissionGroup: ParsedPermissionGroup,
-        state: MutableAccessState = oldState
-    ) {
-        state.mutateSystemState().mutatePermissionGroups()[parsedPermissionGroup.name] =
-            PackageInfoUtils.generatePermissionGroupInfo(
-                parsedPermissionGroup,
-                PackageManager.GET_META_DATA.toLong()
-            )!!
-    }
-
-    private fun getPermission(
-        permissionName: String,
-        state: MutableAccessState = newState
-    ): Permission? = state.systemState.permissions[permissionName]
-
-    private fun getPermissionTree(
-        permissionTreeName: String,
-        state: MutableAccessState = newState
-    ): Permission? = state.systemState.permissionTrees[permissionTreeName]
-
-    private fun getPermissionGroup(
-        permissionGroupName: String,
-        state: MutableAccessState = newState
-    ): PermissionGroupInfo? = state.systemState.permissionGroups[permissionGroupName]
-
-    private fun getPermissionFlags(
-        appId: Int,
-        userId: Int,
-        permissionName: String,
-        state: MutableAccessState = newState
-    ): Int =
-        state.userStates[userId]?.appIdPermissionFlags?.get(appId).getWithDefault(permissionName, 0)
-
-    private fun setPermissionFlags(
-        appId: Int,
-        userId: Int,
-        permissionName: String,
-        flags: Int,
-        state: MutableAccessState = oldState
-    ) =
-        state.mutateUserState(userId)!!.mutateAppIdPermissionFlags().mutateOrPut(appId) {
-            MutableIndexedMap()
-        }.put(permissionName, flags)
-
-    companion object {
-        private const val PACKAGE_NAME_0 = "packageName0"
-        private const val PACKAGE_NAME_1 = "packageName1"
-        private const val MISSING_ANDROID_PACKAGE = "missingAndroidPackage"
-        private const val PLATFORM_PACKAGE_NAME = "android"
-
-        private const val APP_ID_0 = 0
-        private const val APP_ID_1 = 1
-        private const val PLATFORM_APP_ID = 2
-
-        private const val PERMISSION_GROUP_NAME_0 = "permissionGroupName0"
-        private const val PERMISSION_GROUP_NAME_1 = "permissionGroupName1"
-
-        private const val PERMISSION_TREE_NAME = "permissionTree"
-
-        private const val PERMISSION_NAME_0 = "permissionName0"
-        private const val PERMISSION_NAME_1 = "permissionName1"
-        private const val PERMISSION_NAME_2 = "permissionName2"
-        private const val PERMISSION_READ_EXTERNAL_STORAGE =
-            Manifest.permission.READ_EXTERNAL_STORAGE
-        private const val PERMISSION_POST_NOTIFICATIONS =
-            Manifest.permission.POST_NOTIFICATIONS
-        private const val PERMISSION_BLUETOOTH_CONNECT =
-            Manifest.permission.BLUETOOTH_CONNECT
-        private const val PERMISSION_ACCESS_BACKGROUND_LOCATION =
-            Manifest.permission.ACCESS_BACKGROUND_LOCATION
-        private const val PERMISSION_ACCESS_MEDIA_LOCATION =
-            Manifest.permission.ACCESS_MEDIA_LOCATION
-
-        private const val USER_ID_0 = 0
-    }
-}
diff --git a/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/BaseAppIdPermissionPolicyTest.kt b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/BaseAppIdPermissionPolicyTest.kt
new file mode 100644
index 0000000..7966c5c
--- /dev/null
+++ b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/BaseAppIdPermissionPolicyTest.kt
@@ -0,0 +1,446 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.permission.test
+
+import android.Manifest
+import android.content.pm.PackageManager
+import android.content.pm.PermissionGroupInfo
+import android.content.pm.PermissionInfo
+import android.content.pm.SigningDetails
+import android.os.Build
+import android.os.Bundle
+import android.util.ArrayMap
+import android.util.SparseArray
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.modules.utils.testing.ExtendedMockitoRule
+import com.android.server.extendedtestutils.wheneverStatic
+import com.android.server.permission.access.MutableAccessState
+import com.android.server.permission.access.MutableUserState
+import com.android.server.permission.access.MutateStateScope
+import com.android.server.permission.access.immutable.* // ktlint-disable no-wildcard-imports
+import com.android.server.permission.access.permission.AppIdPermissionPolicy
+import com.android.server.permission.access.permission.Permission
+import com.android.server.permission.access.permission.PermissionFlags
+import com.android.server.permission.access.util.hasBits
+import com.android.server.pm.parsing.PackageInfoUtils
+import com.android.server.pm.pkg.AndroidPackage
+import com.android.server.pm.pkg.PackageState
+import com.android.server.pm.pkg.PackageUserState
+import com.android.server.pm.pkg.component.ParsedPermission
+import com.android.server.pm.pkg.component.ParsedPermissionGroup
+import com.android.server.testutils.any
+import com.android.server.testutils.mock
+import com.android.server.testutils.whenever
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyLong
+
+/**
+ * Mocking unit test for AppIdPermissionPolicy.
+ */
+@RunWith(AndroidJUnit4::class)
+open class BaseAppIdPermissionPolicyTest {
+    protected lateinit var oldState: MutableAccessState
+    protected lateinit var newState: MutableAccessState
+
+    protected val defaultPermissionGroup = mockParsedPermissionGroup(
+        PERMISSION_GROUP_NAME_0,
+        PACKAGE_NAME_0
+    )
+    protected val defaultPermissionTree = mockParsedPermission(
+        PERMISSION_TREE_NAME,
+        PACKAGE_NAME_0,
+        isTree = true
+    )
+    protected val defaultPermission = mockParsedPermission(PERMISSION_NAME_0, PACKAGE_NAME_0)
+
+    protected val appIdPermissionPolicy = AppIdPermissionPolicy()
+
+    @Rule
+    @JvmField
+    val extendedMockitoRule = ExtendedMockitoRule.Builder(this)
+        .spyStatic(PackageInfoUtils::class.java)
+        .build()
+
+    @Before
+    open fun setUp() {
+        oldState = MutableAccessState()
+        createUserState(USER_ID_0)
+        oldState.mutateExternalState().setPackageStates(ArrayMap())
+        oldState.mutateExternalState().setDisabledSystemPackageStates(ArrayMap())
+        mockPackageInfoUtilsGeneratePermissionInfo()
+        mockPackageInfoUtilsGeneratePermissionGroupInfo()
+    }
+
+    protected fun createUserState(userId: Int) {
+        oldState.mutateExternalState().mutateUserIds().add(userId)
+        oldState.mutateUserStatesNoWrite().put(userId, MutableUserState())
+    }
+
+    private fun mockPackageInfoUtilsGeneratePermissionInfo() {
+        wheneverStatic {
+            PackageInfoUtils.generatePermissionInfo(any(ParsedPermission::class.java), anyLong())
+        }.thenAnswer { invocation ->
+            val parsedPermission = invocation.getArgument<ParsedPermission>(0)
+            val generateFlags = invocation.getArgument<Long>(1)
+            PermissionInfo(parsedPermission.backgroundPermission).apply {
+                name = parsedPermission.name
+                packageName = parsedPermission.packageName
+                metaData = if (generateFlags.toInt().hasBits(PackageManager.GET_META_DATA)) {
+                    parsedPermission.metaData
+                } else {
+                    null
+                }
+                @Suppress("DEPRECATION")
+                protectionLevel = parsedPermission.protectionLevel
+                group = parsedPermission.group
+                flags = parsedPermission.flags
+            }
+        }
+    }
+
+    private fun mockPackageInfoUtilsGeneratePermissionGroupInfo() {
+        wheneverStatic {
+            PackageInfoUtils.generatePermissionGroupInfo(
+                any(ParsedPermissionGroup::class.java),
+                anyLong()
+            )
+        }.thenAnswer { invocation ->
+            val parsedPermissionGroup = invocation.getArgument<ParsedPermissionGroup>(0)
+            val generateFlags = invocation.getArgument<Long>(1)
+            @Suppress("DEPRECATION")
+            PermissionGroupInfo().apply {
+                name = parsedPermissionGroup.name
+                packageName = parsedPermissionGroup.packageName
+                metaData = if (generateFlags.toInt().hasBits(PackageManager.GET_META_DATA)) {
+                    parsedPermissionGroup.metaData
+                } else {
+                    null
+                }
+                flags = parsedPermissionGroup.flags
+            }
+        }
+    }
+
+    @Test
+    fun testOnAppIdRemoved_appIdIsRemoved_permissionFlagsCleared() {
+        val parsedPermission = mockParsedPermission(PERMISSION_NAME_0, PACKAGE_NAME_0)
+        val permissionOwnerPackageState = mockPackageState(
+            APP_ID_0,
+            mockAndroidPackage(PACKAGE_NAME_0, permissions = listOf(parsedPermission))
+        )
+        val requestingPackageState = mockPackageState(
+            APP_ID_1,
+            mockAndroidPackage(PACKAGE_NAME_1, requestedPermissions = setOf(PERMISSION_NAME_0))
+        )
+        addPackageState(permissionOwnerPackageState)
+        addPackageState(requestingPackageState)
+        addPermission(parsedPermission)
+        setPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0, PermissionFlags.INSTALL_GRANTED)
+
+        mutateState {
+            with(appIdPermissionPolicy) {
+                onAppIdRemoved(APP_ID_1)
+            }
+        }
+
+        val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
+        val expectedNewFlags = 0
+        assertWithMessage(
+            "After onAppIdRemoved() is called for appId $APP_ID_1 that requests a permission" +
+                " owns by appId $APP_ID_0 with existing permission flags. The actual permission" +
+                " flags $actualFlags should be null"
+        )
+            .that(actualFlags)
+            .isEqualTo(expectedNewFlags)
+    }
+
+    @Test
+    fun testOnPackageRemoved_packageIsRemoved_permissionsAreTrimmedAndStatesAreEvaluated() {
+        // TODO
+        // shouldn't reuse test cases because it's really different despite it's also for
+        // trim permission states. It's different because it's package removal
+    }
+
+    @Test
+    fun testOnPackageInstalled_nonSystemAppIsInstalled_upgradeExemptFlagIsCleared() {
+        // TODO
+        // should be fine for it to be its own test cases and not to re-use
+        // clearRestrictedPermissionImplicitExemption
+    }
+
+    @Test
+    fun testOnPackageInstalled_systemAppIsInstalled_upgradeExemptFlagIsRetained() {
+        // TODO
+    }
+
+    @Test
+    fun testOnPackageInstalled_requestedPermissionAlsoRequestedBySystemApp_exemptFlagIsRetained() {
+        // TODO
+    }
+
+    @Test
+    fun testOnPackageInstalled_restrictedPermissionsNotExempt_getsRestrictionFlags() {
+        // TODO
+    }
+
+    @Test
+    fun testOnPackageInstalled_restrictedPermissionsIsExempted_clearsRestrictionFlags() {
+        // TODO
+    }
+
+    @Test
+    fun testOnStateMutated_notEmpty_isCalledForEachListener() {
+        // TODO
+    }
+
+    /**
+     * Mock an AndroidPackage with PACKAGE_NAME_0, PERMISSION_NAME_0 and PERMISSION_GROUP_NAME_0
+     */
+    protected fun mockSimpleAndroidPackage(): AndroidPackage =
+        mockAndroidPackage(
+            PACKAGE_NAME_0,
+            permissionGroups = listOf(defaultPermissionGroup),
+            permissions = listOf(defaultPermissionTree, defaultPermission)
+        )
+
+    protected inline fun mutateState(action: MutateStateScope.() -> Unit) {
+        newState = oldState.toMutable()
+        MutateStateScope(oldState, newState).action()
+    }
+
+    protected fun mockPackageState(
+        appId: Int,
+        packageName: String,
+        isSystem: Boolean = false,
+    ): PackageState =
+        mock {
+            whenever(this.appId).thenReturn(appId)
+            whenever(this.packageName).thenReturn(packageName)
+            whenever(androidPackage).thenReturn(null)
+            whenever(this.isSystem).thenReturn(isSystem)
+        }
+
+    protected fun mockPackageState(
+        appId: Int,
+        androidPackage: AndroidPackage,
+        isSystem: Boolean = false,
+        isPrivileged: Boolean = false,
+        isProduct: Boolean = false,
+        isInstantApp: Boolean = false,
+        isVendor: Boolean = false
+    ): PackageState =
+        mock {
+            whenever(this.appId).thenReturn(appId)
+            whenever(this.androidPackage).thenReturn(androidPackage)
+            val packageName = androidPackage.packageName
+            whenever(this.packageName).thenReturn(packageName)
+            whenever(this.isSystem).thenReturn(isSystem)
+            whenever(this.isPrivileged).thenReturn(isPrivileged)
+            whenever(this.isProduct).thenReturn(isProduct)
+            whenever(this.isVendor).thenReturn(isVendor)
+            val userStates = SparseArray<PackageUserState>().apply {
+                put(USER_ID_0, mock { whenever(this.isInstantApp).thenReturn(isInstantApp) })
+            }
+            whenever(this.userStates).thenReturn(userStates)
+        }
+
+    protected fun mockAndroidPackage(
+        packageName: String,
+        targetSdkVersion: Int = Build.VERSION_CODES.UPSIDE_DOWN_CAKE,
+        isRequestLegacyExternalStorage: Boolean = false,
+        adoptPermissions: List<String> = emptyList(),
+        implicitPermissions: Set<String> = emptySet(),
+        requestedPermissions: Set<String> = emptySet(),
+        permissionGroups: List<ParsedPermissionGroup> = emptyList(),
+        permissions: List<ParsedPermission> = emptyList(),
+        isSignatureMatching: Boolean = false
+    ): AndroidPackage =
+        mock {
+            whenever(this.packageName).thenReturn(packageName)
+            whenever(this.targetSdkVersion).thenReturn(targetSdkVersion)
+            whenever(this.isRequestLegacyExternalStorage).thenReturn(isRequestLegacyExternalStorage)
+            whenever(this.adoptPermissions).thenReturn(adoptPermissions)
+            whenever(this.implicitPermissions).thenReturn(implicitPermissions)
+            whenever(this.requestedPermissions).thenReturn(requestedPermissions)
+            whenever(this.permissionGroups).thenReturn(permissionGroups)
+            whenever(this.permissions).thenReturn(permissions)
+            val signingDetails = mock<SigningDetails> {
+                whenever(
+                    hasCommonSignerWithCapability(any(), any())
+                ).thenReturn(isSignatureMatching)
+                whenever(hasAncestorOrSelf(any())).thenReturn(isSignatureMatching)
+                whenever(
+                    checkCapability(any<SigningDetails>(), any())
+                ).thenReturn(isSignatureMatching)
+            }
+            whenever(this.signingDetails).thenReturn(signingDetails)
+        }
+
+    protected fun mockParsedPermission(
+        permissionName: String,
+        packageName: String,
+        backgroundPermission: String? = null,
+        group: String? = null,
+        protectionLevel: Int = PermissionInfo.PROTECTION_NORMAL,
+        flags: Int = 0,
+        isTree: Boolean = false
+    ): ParsedPermission =
+        mock {
+            whenever(name).thenReturn(permissionName)
+            whenever(this.packageName).thenReturn(packageName)
+            whenever(metaData).thenReturn(Bundle())
+            whenever(this.backgroundPermission).thenReturn(backgroundPermission)
+            whenever(this.group).thenReturn(group)
+            whenever(this.protectionLevel).thenReturn(protectionLevel)
+            whenever(this.flags).thenReturn(flags)
+            whenever(this.isTree).thenReturn(isTree)
+        }
+
+    protected fun mockParsedPermissionGroup(
+        permissionGroupName: String,
+        packageName: String,
+    ): ParsedPermissionGroup =
+        mock {
+            whenever(name).thenReturn(permissionGroupName)
+            whenever(this.packageName).thenReturn(packageName)
+            whenever(metaData).thenReturn(Bundle())
+        }
+
+    protected fun addPackageState(
+        packageState: PackageState,
+        state: MutableAccessState = oldState
+    ) {
+        state.mutateExternalState().apply {
+            setPackageStates(
+                packageStates.toMutableMap().apply {
+                    put(packageState.packageName, packageState)
+                }
+            )
+            mutateAppIdPackageNames().mutateOrPut(packageState.appId) { MutableIndexedListSet() }
+                .add(packageState.packageName)
+        }
+    }
+
+    protected fun addDisabledSystemPackageState(
+        packageState: PackageState,
+        state: MutableAccessState = oldState
+    ) = state.mutateExternalState().apply {
+        (disabledSystemPackageStates as ArrayMap)[packageState.packageName] = packageState
+    }
+
+    protected fun addPermission(
+        parsedPermission: ParsedPermission,
+        type: Int = Permission.TYPE_MANIFEST,
+        isReconciled: Boolean = true,
+        state: MutableAccessState = oldState
+    ) {
+        val permissionInfo = PackageInfoUtils.generatePermissionInfo(
+            parsedPermission,
+            PackageManager.GET_META_DATA.toLong()
+        )!!
+        val appId = state.externalState.packageStates[permissionInfo.packageName]!!.appId
+        val permission = Permission(permissionInfo, isReconciled, type, appId)
+        if (parsedPermission.isTree) {
+            state.mutateSystemState().mutatePermissionTrees()[permission.name] = permission
+        } else {
+            state.mutateSystemState().mutatePermissions()[permission.name] = permission
+        }
+    }
+
+    protected fun addPermissionGroup(
+        parsedPermissionGroup: ParsedPermissionGroup,
+        state: MutableAccessState = oldState
+    ) {
+        state.mutateSystemState().mutatePermissionGroups()[parsedPermissionGroup.name] =
+            PackageInfoUtils.generatePermissionGroupInfo(
+                parsedPermissionGroup,
+                PackageManager.GET_META_DATA.toLong()
+            )!!
+    }
+
+    protected fun getPermission(
+        permissionName: String,
+        state: MutableAccessState = newState
+    ): Permission? = state.systemState.permissions[permissionName]
+
+    protected fun getPermissionTree(
+        permissionTreeName: String,
+        state: MutableAccessState = newState
+    ): Permission? = state.systemState.permissionTrees[permissionTreeName]
+
+    protected fun getPermissionGroup(
+        permissionGroupName: String,
+        state: MutableAccessState = newState
+    ): PermissionGroupInfo? = state.systemState.permissionGroups[permissionGroupName]
+
+    protected fun getPermissionFlags(
+        appId: Int,
+        userId: Int,
+        permissionName: String,
+        state: MutableAccessState = newState
+    ): Int =
+        state.userStates[userId]?.appIdPermissionFlags?.get(appId).getWithDefault(permissionName, 0)
+
+    protected fun setPermissionFlags(
+        appId: Int,
+        userId: Int,
+        permissionName: String,
+        flags: Int,
+        state: MutableAccessState = oldState
+    ) =
+        state.mutateUserState(userId)!!.mutateAppIdPermissionFlags().mutateOrPut(appId) {
+            MutableIndexedMap()
+        }.put(permissionName, flags)
+
+    companion object {
+        @JvmStatic protected val PACKAGE_NAME_0 = "packageName0"
+        @JvmStatic protected val PACKAGE_NAME_1 = "packageName1"
+        @JvmStatic protected val PACKAGE_NAME_2 = "packageName2"
+        @JvmStatic protected val MISSING_ANDROID_PACKAGE = "missingAndroidPackage"
+        @JvmStatic protected val PLATFORM_PACKAGE_NAME = "android"
+
+        @JvmStatic protected val APP_ID_0 = 0
+        @JvmStatic protected val APP_ID_1 = 1
+        @JvmStatic protected val PLATFORM_APP_ID = 2
+
+        @JvmStatic protected val PERMISSION_GROUP_NAME_0 = "permissionGroupName0"
+        @JvmStatic protected val PERMISSION_GROUP_NAME_1 = "permissionGroupName1"
+
+        @JvmStatic protected val PERMISSION_TREE_NAME = "permissionTree"
+
+        @JvmStatic protected val PERMISSION_NAME_0 = "permissionName0"
+        @JvmStatic protected val PERMISSION_NAME_1 = "permissionName1"
+        @JvmStatic protected val PERMISSION_NAME_2 = "permissionName2"
+        @JvmStatic protected val PERMISSION_READ_EXTERNAL_STORAGE =
+            Manifest.permission.READ_EXTERNAL_STORAGE
+        @JvmStatic protected val PERMISSION_POST_NOTIFICATIONS =
+            Manifest.permission.POST_NOTIFICATIONS
+        @JvmStatic protected val PERMISSION_BLUETOOTH_CONNECT =
+            Manifest.permission.BLUETOOTH_CONNECT
+        @JvmStatic protected val PERMISSION_ACCESS_BACKGROUND_LOCATION =
+            Manifest.permission.ACCESS_BACKGROUND_LOCATION
+        @JvmStatic protected val PERMISSION_ACCESS_MEDIA_LOCATION =
+            Manifest.permission.ACCESS_MEDIA_LOCATION
+
+        @JvmStatic protected val USER_ID_0 = 0
+        @JvmStatic protected val USER_ID_NEW = 1
+    }
+}
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/mode/DisplayModeDirectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
index fbad369..b8c18e07 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
@@ -91,6 +91,7 @@
 import com.android.internal.util.test.FakeSettingsProviderRule;
 import com.android.server.display.DisplayDeviceConfig;
 import com.android.server.display.TestUtils;
+import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.mode.DisplayModeDirector.BrightnessObserver;
 import com.android.server.display.mode.DisplayModeDirector.DesiredDisplayModeSpecs;
 import com.android.server.sensors.SensorManagerInternal;
@@ -110,7 +111,9 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
@@ -121,10 +124,114 @@
 @SmallTest
 @RunWith(JUnitParamsRunner.class)
 public class DisplayModeDirectorTest {
-    // The tolerance within which we consider something approximately equals.
+    public static Collection<Object[]> getAppRequestedSizeTestCases() {
+        var appRequestedSizeTestCases = Arrays.asList(new Object[][] {
+                {DEFAULT_MODE_75.getModeId(), Float.POSITIVE_INFINITY,
+                        DEFAULT_MODE_75.getRefreshRate(), Map.of()},
+                {APP_MODE_HIGH_90.getModeId(), Float.POSITIVE_INFINITY,
+                        APP_MODE_HIGH_90.getRefreshRate(),
+                        Map.of(
+                                Vote.PRIORITY_APP_REQUEST_SIZE,
+                                Vote.forSize(APP_MODE_HIGH_90.getPhysicalWidth(),
+                                        APP_MODE_HIGH_90.getPhysicalHeight()),
+                                Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
+                                Vote.forBaseModeRefreshRate(APP_MODE_HIGH_90.getRefreshRate()))},
+                {LIMIT_MODE_70.getModeId(), Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY,
+                        Map.of(
+                                Vote.PRIORITY_APP_REQUEST_SIZE,
+                                Vote.forSize(APP_MODE_HIGH_90.getPhysicalWidth(),
+                                        APP_MODE_HIGH_90.getPhysicalHeight()),
+                                Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
+                                Vote.forBaseModeRefreshRate(APP_MODE_HIGH_90.getRefreshRate()),
+                                Vote.PRIORITY_LOW_POWER_MODE,
+                                Vote.forSize(LIMIT_MODE_70.getPhysicalWidth(),
+                                        LIMIT_MODE_70.getPhysicalHeight()))},
+                {LIMIT_MODE_70.getModeId(), LIMIT_MODE_70.getRefreshRate(),
+                        LIMIT_MODE_70.getRefreshRate(),
+                        Map.of(
+                                Vote.PRIORITY_APP_REQUEST_SIZE,
+                                Vote.forSize(APP_MODE_65.getPhysicalWidth(),
+                                        APP_MODE_65.getPhysicalHeight()),
+                                Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
+                                Vote.forBaseModeRefreshRate(APP_MODE_65.getRefreshRate()),
+                                Vote.PRIORITY_LOW_POWER_MODE,
+                                Vote.forSize(LIMIT_MODE_70.getPhysicalWidth(),
+                                        LIMIT_MODE_70.getPhysicalHeight()))},
+                {LIMIT_MODE_70.getModeId(), LIMIT_MODE_70.getRefreshRate(),
+                        LIMIT_MODE_70.getRefreshRate(),
+                        Map.of(
+                                Vote.PRIORITY_APP_REQUEST_SIZE,
+                                Vote.forSize(APP_MODE_65.getPhysicalWidth(),
+                                        APP_MODE_65.getPhysicalHeight()),
+                                Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
+                                Vote.forBaseModeRefreshRate(APP_MODE_65.getRefreshRate()),
+                                Vote.PRIORITY_LOW_POWER_MODE,
+                                Vote.forSizeAndPhysicalRefreshRatesRange(
+                                    0, 0,
+                                    LIMIT_MODE_70.getPhysicalWidth(),
+                                    LIMIT_MODE_70.getPhysicalHeight(),
+                                    0, Float.POSITIVE_INFINITY)), false},
+                {APP_MODE_65.getModeId(), APP_MODE_65.getRefreshRate(),
+                        APP_MODE_65.getRefreshRate(),
+                        Map.of(
+                                Vote.PRIORITY_APP_REQUEST_SIZE,
+                                Vote.forSize(APP_MODE_65.getPhysicalWidth(),
+                                        APP_MODE_65.getPhysicalHeight()),
+                                Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
+                                Vote.forBaseModeRefreshRate(APP_MODE_65.getRefreshRate()),
+                                Vote.PRIORITY_LOW_POWER_MODE,
+                                Vote.forSizeAndPhysicalRefreshRatesRange(
+                                    0, 0,
+                                    LIMIT_MODE_70.getPhysicalWidth(),
+                                    LIMIT_MODE_70.getPhysicalHeight(),
+                                    0, Float.POSITIVE_INFINITY)), true}});
+
+        final var res = new ArrayList<Object[]>(appRequestedSizeTestCases.size() * 2);
+
+        // Add additional argument for displayResolutionRangeVotingEnabled=false if not present.
+        for (var testCaseArrayArgs : appRequestedSizeTestCases) {
+            if (testCaseArrayArgs.length == 4) {
+                var testCaseListArgs = new ArrayList<>(Arrays.asList(testCaseArrayArgs));
+                testCaseListArgs.add(/* displayResolutionRangeVotingEnabled */ false);
+                res.add(testCaseListArgs.toArray());
+            } else {
+                res.add(testCaseArrayArgs);
+            }
+        }
+
+        // Add additional argument for displayResolutionRangeVotingEnabled=true if not present.
+        for (var testCaseArrayArgs : appRequestedSizeTestCases) {
+            if (testCaseArrayArgs.length == 4) {
+                var testCaseListArgs = new ArrayList<>(Arrays.asList(testCaseArrayArgs));
+                testCaseListArgs.add(/* displayResolutionRangeVotingEnabled */ true);
+                res.add(testCaseListArgs.toArray());
+            }
+        }
+
+        return res;
+    }
+
     private static final String TAG = "DisplayModeDirectorTest";
     private static final boolean DEBUG = false;
     private static final float FLOAT_TOLERANCE = 0.01f;
+
+    private static final Display.Mode APP_MODE_65 = new Display.Mode(
+            /*modeId=*/65, /*width=*/1900, /*height=*/1900, 65);
+    private static final Display.Mode LIMIT_MODE_70 = new Display.Mode(
+            /*modeId=*/70, /*width=*/2000, /*height=*/2000, 70);
+    private static final Display.Mode DEFAULT_MODE_75 = new Display.Mode(
+            /*modeId=*/75, /*width=*/2500, /*height=*/2500, 75);
+    private static final Display.Mode APP_MODE_HIGH_90 = new Display.Mode(
+            /*modeId=*/90, /*width=*/3000, /*height=*/3000, 90);
+    private static final Display.Mode[] TEST_MODES = new Display.Mode[] {
+        new Display.Mode(
+            /*modeId=*/60, /*width=*/1900, /*height=*/1900, 60),
+        APP_MODE_65,
+        LIMIT_MODE_70,
+        DEFAULT_MODE_75,
+        APP_MODE_HIGH_90
+    };
+
     private static final int DISPLAY_ID = Display.DEFAULT_DISPLAY;
     private static final int MODE_ID = 1;
     private static final float TRANSITION_POINT = 0.763f;
@@ -142,6 +249,8 @@
     public SensorManagerInternal mSensorManagerInternalMock;
     @Mock
     public DisplayManagerInternal mDisplayManagerInternalMock;
+    @Mock
+    private DisplayManagerFlags mDisplayManagerFlags;
 
     @Before
     public void setUp() throws Exception {
@@ -177,7 +286,7 @@
     private DisplayModeDirector createDirectorFromModeArray(Display.Mode[] modes,
             Display.Mode defaultMode) {
         DisplayModeDirector director =
-                new DisplayModeDirector(mContext, mHandler, mInjector);
+                new DisplayModeDirector(mContext, mHandler, mInjector, mDisplayManagerFlags);
         director.setLoggingEnabled(true);
         SparseArray<Display.Mode[]> supportedModesByDisplay = new SparseArray<>();
         supportedModesByDisplay.put(DISPLAY_ID, modes);
@@ -219,9 +328,8 @@
         // should take precedence over lower priority votes.
         {
             int minFps = 60;
-            int maxFps = 90;
-            director = createDirectorFromFpsRange(60, 90);
-            assertTrue(2 * numPriorities < maxFps - minFps + 1);
+            int maxFps = minFps + 2 * numPriorities;
+            director = createDirectorFromFpsRange(minFps, maxFps);
             SparseArray<Vote> votes = new SparseArray<>();
             SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
             votesByDisplay.put(DISPLAY_ID, votes);
@@ -472,6 +580,7 @@
         assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(90);
     }
 
+    /** Resolution range voting disabled */
     @Test
     public void testAppRequestRefreshRateRange() {
         // Confirm that the app request range doesn't include flicker or min refresh rate settings,
@@ -530,6 +639,33 @@
         assertThat(desiredSpecs.appRequest.physical.max).isAtLeast(90f);
     }
 
+    /** Tests for app requested size */
+    @Parameters(method = "getAppRequestedSizeTestCases")
+    @Test
+    public void testAppRequestedSize(final int expectedBaseModeId,
+                final float expectedPhysicalRefreshRate,
+                final float expectedAppRequestedRefreshRate,
+                final Map<Integer, Vote> votesWithPriorities,
+                final boolean displayResolutionRangeVotingEnabled) {
+        when(mDisplayManagerFlags.isDisplayResolutionRangeVotingEnabled())
+                .thenReturn(displayResolutionRangeVotingEnabled);
+        DisplayModeDirector director = createDirectorFromModeArray(TEST_MODES, DEFAULT_MODE_75);
+
+        SparseArray<Vote> votes = new SparseArray<>();
+        votesWithPriorities.forEach(votes::put);
+
+        SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
+        votesByDisplay.put(DISPLAY_ID, votes);
+        director.injectVotesByDisplay(votesByDisplay);
+
+        var desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.baseModeId).isEqualTo(expectedBaseModeId);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(0);
+        assertThat(desiredSpecs.primary.physical.max).isAtLeast(expectedPhysicalRefreshRate);
+        assertThat(desiredSpecs.appRequest.physical.min).isAtMost(0);
+        assertThat(desiredSpecs.appRequest.physical.max).isAtLeast(expectedAppRequestedRefreshRate);
+    }
+
     void verifySpecsWithRefreshRateSettings(DisplayModeDirector director, float minFps,
             float peakFps, float defaultFps, RefreshRateRanges primary,
             RefreshRateRanges appRequest) {
@@ -843,7 +979,7 @@
     @Test
     public void testStaleAppRequestSize() {
         DisplayModeDirector director =
-                new DisplayModeDirector(mContext, mHandler, mInjector);
+                new DisplayModeDirector(mContext, mHandler, mInjector, mDisplayManagerFlags);
         Display.Mode[] modes = new Display.Mode[] {
                 new Display.Mode(1, 1280, 720, 60),
         };
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayObserverTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayObserverTest.java
new file mode 100644
index 0000000..ff91d34
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayObserverTest.java
@@ -0,0 +1,446 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display.mode;
+
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.Mode.INVALID_MODE_ID;
+
+
+import static com.android.server.display.mode.DisplayModeDirector.SYNCHRONIZED_REFRESH_RATE_TOLERANCE;
+import static com.android.server.display.mode.Vote.PRIORITY_LIMIT_MODE;
+import static com.android.server.display.mode.Vote.PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE;
+import static com.android.server.display.mode.Vote.PRIORITY_SYNCHRONIZED_REFRESH_RATE;
+import static com.android.server.display.mode.VotesStorage.GLOBAL_ID;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.res.Resources;
+import android.hardware.display.DisplayManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.DeviceConfigInterface;
+import android.view.Display;
+import android.view.DisplayInfo;
+
+import androidx.annotation.Nullable;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.R;
+import com.android.server.display.feature.DisplayManagerFlags;
+import com.android.server.sensors.SensorManagerInternal;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import junitparams.JUnitParamsRunner;
+
+
+@SmallTest
+@RunWith(JUnitParamsRunner.class)
+public class DisplayObserverTest {
+    private static final int EXTERNAL_DISPLAY = 1;
+    private static final int MAX_WIDTH = 1920;
+    private static final int MAX_HEIGHT = 1080;
+    private static final int MAX_REFRESH_RATE = 60;
+
+    private final Display.Mode[] mInternalDisplayModes = new Display.Mode[] {
+            new Display.Mode(/*modeId=*/ 0, MAX_WIDTH / 2, MAX_HEIGHT / 2,
+                    (float) MAX_REFRESH_RATE / 2),
+            new Display.Mode(/*modeId=*/ 1, MAX_WIDTH / 2, MAX_HEIGHT / 2,
+                    MAX_REFRESH_RATE),
+            new Display.Mode(/*modeId=*/ 2, MAX_WIDTH / 2, MAX_HEIGHT / 2,
+                    MAX_REFRESH_RATE * 2),
+            new Display.Mode(/*modeId=*/ 3, MAX_WIDTH, MAX_HEIGHT, MAX_REFRESH_RATE * 2),
+            new Display.Mode(/*modeId=*/ 4, MAX_WIDTH, MAX_HEIGHT, MAX_REFRESH_RATE),
+            new Display.Mode(/*modeId=*/ 5, MAX_WIDTH * 2, MAX_HEIGHT * 2,
+                    MAX_REFRESH_RATE),
+            new Display.Mode(/*modeId=*/ 6, MAX_WIDTH / 2, MAX_HEIGHT / 2,
+                    MAX_REFRESH_RATE * 3),
+    };
+
+    private final Display.Mode[] mExternalDisplayModes = new Display.Mode[] {
+            new Display.Mode(/*modeId=*/ 0, MAX_WIDTH / 2, MAX_HEIGHT / 2,
+                    (float) MAX_REFRESH_RATE / 2),
+            new Display.Mode(/*modeId=*/ 1, MAX_WIDTH / 2, MAX_HEIGHT / 2,
+                    MAX_REFRESH_RATE),
+            new Display.Mode(/*modeId=*/ 2, MAX_WIDTH / 2, MAX_HEIGHT / 2,
+                    MAX_REFRESH_RATE * 2),
+            new Display.Mode(/*modeId=*/ 3, MAX_WIDTH, MAX_HEIGHT, MAX_REFRESH_RATE * 2),
+            new Display.Mode(/*modeId=*/ 4, MAX_WIDTH, MAX_HEIGHT, MAX_REFRESH_RATE),
+            new Display.Mode(/*modeId=*/ 5, MAX_WIDTH * 2, MAX_HEIGHT * 2,
+                    MAX_REFRESH_RATE),
+    };
+
+    private DisplayModeDirector mDmd;
+    private Context mContext;
+    private DisplayModeDirector.Injector mInjector;
+    private Handler mHandler;
+    private DisplayManager.DisplayListener mObserver;
+    private Resources mResources;
+    @Mock
+    private DisplayManagerFlags mDisplayManagerFlags;
+    private int mExternalDisplayUserPreferredModeId = INVALID_MODE_ID;
+    private int mInternalDisplayUserPreferredModeId = INVALID_MODE_ID;
+    private Display mDefaultDisplay;
+    private Display mExternalDisplay;
+
+    /** Setup tests. */
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mHandler = new Handler(Looper.getMainLooper());
+        mContext = spy(new ContextWrapper(ApplicationProvider.getApplicationContext()));
+        mResources = mock(Resources.class);
+        when(mContext.getResources()).thenReturn(mResources);
+        when(mResources.getInteger(R.integer.config_externalDisplayPeakRefreshRate))
+                .thenReturn(0);
+        when(mResources.getInteger(R.integer.config_externalDisplayPeakWidth))
+                .thenReturn(0);
+        when(mResources.getInteger(R.integer.config_externalDisplayPeakHeight))
+                .thenReturn(0);
+        when(mResources.getBoolean(R.bool.config_refreshRateSynchronizationEnabled))
+                .thenReturn(false);
+
+        // Necessary configs to initialize DisplayModeDirector
+        when(mResources.getIntArray(R.array.config_brightnessThresholdsOfPeakRefreshRate))
+                .thenReturn(new int[]{5});
+        when(mResources.getIntArray(R.array.config_ambientThresholdsOfPeakRefreshRate))
+                .thenReturn(new int[]{10});
+        when(mResources.getIntArray(
+                R.array.config_highDisplayBrightnessThresholdsOfFixedRefreshRate))
+                .thenReturn(new int[]{250});
+        when(mResources.getIntArray(
+                R.array.config_highAmbientBrightnessThresholdsOfFixedRefreshRate))
+                .thenReturn(new int[]{7000});
+    }
+
+    /** No vote for user preferred mode */
+    @Test
+    public void testExternalDisplay_notVotedUserPreferredMode() {
+        var preferredMode = mExternalDisplayModes[5];
+        mExternalDisplayUserPreferredModeId = preferredMode.getModeId();
+
+        init();
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+
+        // Testing that the vote is not added when display is added because feature is disabled
+        mObserver.onDisplayAdded(EXTERNAL_DISPLAY);
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+
+        // Testing that the vote is not present after display is removed
+        mObserver.onDisplayRemoved(EXTERNAL_DISPLAY);
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+
+        // Testing that the vote is not added when display is changed because feature is disabled
+        mObserver.onDisplayChanged(EXTERNAL_DISPLAY);
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+    }
+
+    /** Vote for user preferred mode */
+    @Test
+    public void testExternalDisplay_voteUserPreferredMode() {
+        when(mDisplayManagerFlags.isUserPreferredModeVoteEnabled()).thenReturn(true);
+        var preferredMode = mExternalDisplayModes[5];
+        mExternalDisplayUserPreferredModeId = preferredMode.getModeId();
+        var expectedVote = Vote.forSize(
+                        preferredMode.getPhysicalWidth(),
+                        preferredMode.getPhysicalHeight());
+        init();
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+        mObserver.onDisplayAdded(EXTERNAL_DISPLAY);
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(expectedVote);
+
+        mExternalDisplayUserPreferredModeId = INVALID_MODE_ID;
+        mObserver.onDisplayChanged(EXTERNAL_DISPLAY);
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+
+        preferredMode = mExternalDisplayModes[4];
+        mExternalDisplayUserPreferredModeId = preferredMode.getModeId();
+        expectedVote = Vote.forSize(
+                        preferredMode.getPhysicalWidth(),
+                        preferredMode.getPhysicalHeight());
+        mObserver.onDisplayChanged(EXTERNAL_DISPLAY);
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(expectedVote);
+
+        // Testing that the vote is removed.
+        mObserver.onDisplayRemoved(EXTERNAL_DISPLAY);
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+    }
+
+    /** External display: Do not apply limit to user preferred mode */
+    @Test
+    public void testExternalDisplay_doNotApplyLimitToUserPreferredMode() {
+        when(mDisplayManagerFlags.isUserPreferredModeVoteEnabled()).thenReturn(true);
+        when(mDisplayManagerFlags.isExternalDisplayLimitModeEnabled()).thenReturn(true);
+        when(mResources.getInteger(R.integer.config_externalDisplayPeakRefreshRate))
+                .thenReturn(MAX_REFRESH_RATE);
+        when(mResources.getInteger(R.integer.config_externalDisplayPeakWidth))
+                .thenReturn(MAX_WIDTH);
+        when(mResources.getInteger(R.integer.config_externalDisplayPeakHeight))
+                .thenReturn(MAX_HEIGHT);
+
+        var preferredMode = mExternalDisplayModes[5];
+        mExternalDisplayUserPreferredModeId = preferredMode.getModeId();
+        var expectedResolutionVote = Vote.forSize(preferredMode.getPhysicalWidth(),
+                preferredMode.getPhysicalHeight());
+        init();
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+        mObserver.onDisplayAdded(EXTERNAL_DISPLAY);
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(expectedResolutionVote);
+
+        // Testing that the vote is removed.
+        mObserver.onDisplayRemoved(EXTERNAL_DISPLAY);
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+    }
+
+    /** Default display: Do not apply limit to user preferred mode */
+    @Test
+    public void testDefaultDisplayAdded_notAppliedLimitToUserPreferredMode() {
+        when(mDisplayManagerFlags.isUserPreferredModeVoteEnabled()).thenReturn(true);
+        when(mDisplayManagerFlags.isExternalDisplayLimitModeEnabled()).thenReturn(true);
+        when(mResources.getInteger(R.integer.config_externalDisplayPeakRefreshRate))
+                .thenReturn(MAX_REFRESH_RATE);
+        when(mResources.getInteger(R.integer.config_externalDisplayPeakWidth))
+                .thenReturn(MAX_WIDTH);
+        when(mResources.getInteger(R.integer.config_externalDisplayPeakHeight))
+                .thenReturn(MAX_HEIGHT);
+        var preferredMode = mInternalDisplayModes[5];
+        mInternalDisplayUserPreferredModeId = preferredMode.getModeId();
+        var expectedResolutionVote = Vote.forSize(preferredMode.getPhysicalWidth(),
+                        preferredMode.getPhysicalHeight());
+        init();
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+        mObserver.onDisplayAdded(DEFAULT_DISPLAY);
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(expectedResolutionVote);
+        mObserver.onDisplayRemoved(DEFAULT_DISPLAY);
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+    }
+
+    /** Default display added, no mode limit set */
+    @Test
+    public void testDefaultDisplayAdded() {
+        when(mDisplayManagerFlags.isUserPreferredModeVoteEnabled()).thenReturn(true);
+        when(mDisplayManagerFlags.isExternalDisplayLimitModeEnabled()).thenReturn(true);
+        when(mResources.getInteger(R.integer.config_externalDisplayPeakRefreshRate))
+                .thenReturn(MAX_REFRESH_RATE);
+        when(mResources.getInteger(R.integer.config_externalDisplayPeakWidth))
+                .thenReturn(MAX_WIDTH);
+        when(mResources.getInteger(R.integer.config_externalDisplayPeakHeight))
+                .thenReturn(MAX_HEIGHT);
+        init();
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null);
+        mObserver.onDisplayAdded(DEFAULT_DISPLAY);
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null);
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null);
+    }
+
+    /** External display added, apply resolution refresh rate limit */
+    @Test
+    public void testExternalDisplayAdded_applyResolutionRefreshRateLimit() {
+        when(mDisplayManagerFlags.isDisplayResolutionRangeVotingEnabled()).thenReturn(true);
+        when(mDisplayManagerFlags.isUserPreferredModeVoteEnabled()).thenReturn(true);
+        when(mDisplayManagerFlags.isExternalDisplayLimitModeEnabled()).thenReturn(true);
+        when(mResources.getInteger(R.integer.config_externalDisplayPeakRefreshRate))
+                .thenReturn(MAX_REFRESH_RATE);
+        when(mResources.getInteger(R.integer.config_externalDisplayPeakWidth))
+                .thenReturn(MAX_WIDTH);
+        when(mResources.getInteger(R.integer.config_externalDisplayPeakHeight))
+                .thenReturn(MAX_HEIGHT);
+        init();
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null);
+        mObserver.onDisplayAdded(EXTERNAL_DISPLAY);
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null);
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(
+                Vote.forSizeAndPhysicalRefreshRatesRange(0, 0,
+                        MAX_WIDTH, MAX_HEIGHT,
+                        /*minPhysicalRefreshRate=*/ 0, MAX_REFRESH_RATE));
+        mObserver.onDisplayRemoved(EXTERNAL_DISPLAY);
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null);
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null);
+    }
+
+    /** External display added, disabled resolution refresh rate limit. */
+    @Test
+    public void testExternalDisplayAdded_disabledResolutionRefreshRateLimit() {
+        when(mDisplayManagerFlags.isDisplayResolutionRangeVotingEnabled()).thenReturn(true);
+        when(mDisplayManagerFlags.isUserPreferredModeVoteEnabled()).thenReturn(true);
+        when(mDisplayManagerFlags.isExternalDisplayLimitModeEnabled()).thenReturn(true);
+        init();
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null);
+        mObserver.onDisplayAdded(EXTERNAL_DISPLAY);
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null);
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null);
+        mObserver.onDisplayChanged(EXTERNAL_DISPLAY);
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null);
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null);
+        mObserver.onDisplayRemoved(EXTERNAL_DISPLAY);
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null);
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_LIMIT_MODE)).isEqualTo(null);
+    }
+
+    /** External display added, applied refresh rates synchronization */
+    @Test
+    public void testExternalDisplayAdded_appliedRefreshRatesSynchronization() {
+        when(mDisplayManagerFlags.isDisplayResolutionRangeVotingEnabled()).thenReturn(true);
+        when(mDisplayManagerFlags.isUserPreferredModeVoteEnabled()).thenReturn(true);
+        when(mDisplayManagerFlags.isExternalDisplayLimitModeEnabled()).thenReturn(true);
+        when(mDisplayManagerFlags.isDisplaysRefreshRatesSynchronizationEnabled()).thenReturn(true);
+        when(mResources.getBoolean(R.bool.config_refreshRateSynchronizationEnabled))
+                .thenReturn(true);
+        init();
+        assertThat(getVote(GLOBAL_ID, PRIORITY_SYNCHRONIZED_REFRESH_RATE)).isEqualTo(null);
+        mObserver.onDisplayAdded(EXTERNAL_DISPLAY);
+        assertThat(getVote(GLOBAL_ID, PRIORITY_SYNCHRONIZED_REFRESH_RATE)).isEqualTo(
+                Vote.forPhysicalRefreshRates(
+                        MAX_REFRESH_RATE - SYNCHRONIZED_REFRESH_RATE_TOLERANCE,
+                        MAX_REFRESH_RATE + SYNCHRONIZED_REFRESH_RATE_TOLERANCE));
+
+        // Remove external display and check that sync vote is no longer present.
+        mObserver.onDisplayRemoved(EXTERNAL_DISPLAY);
+
+        assertThat(getVote(GLOBAL_ID, PRIORITY_SYNCHRONIZED_REFRESH_RATE)).isEqualTo(null);
+    }
+
+    /** External display added, disabled feature refresh rates synchronization */
+    @Test
+    public void testExternalDisplayAdded_disabledFeatureRefreshRatesSynchronization() {
+        when(mDisplayManagerFlags.isDisplayResolutionRangeVotingEnabled()).thenReturn(true);
+        when(mDisplayManagerFlags.isUserPreferredModeVoteEnabled()).thenReturn(true);
+        when(mDisplayManagerFlags.isExternalDisplayLimitModeEnabled()).thenReturn(true);
+        when(mDisplayManagerFlags.isDisplaysRefreshRatesSynchronizationEnabled()).thenReturn(false);
+        when(mResources.getBoolean(R.bool.config_refreshRateSynchronizationEnabled))
+                .thenReturn(true);
+        init();
+        assertThat(getVote(GLOBAL_ID, PRIORITY_SYNCHRONIZED_REFRESH_RATE)).isEqualTo(null);
+        mObserver.onDisplayAdded(EXTERNAL_DISPLAY);
+        assertThat(getVote(GLOBAL_ID, PRIORITY_SYNCHRONIZED_REFRESH_RATE)).isEqualTo(null);
+    }
+
+    /** External display not applied refresh rates synchronization, because
+     * config_refreshRateSynchronizationEnabled is false. */
+    @Test
+    public void testExternalDisplay_notAppliedRefreshRatesSynchronization() {
+        when(mDisplayManagerFlags.isDisplayResolutionRangeVotingEnabled()).thenReturn(true);
+        when(mDisplayManagerFlags.isUserPreferredModeVoteEnabled()).thenReturn(true);
+        when(mDisplayManagerFlags.isExternalDisplayLimitModeEnabled()).thenReturn(true);
+        when(mDisplayManagerFlags.isDisplaysRefreshRatesSynchronizationEnabled()).thenReturn(true);
+        init();
+        assertThat(getVote(GLOBAL_ID, PRIORITY_SYNCHRONIZED_REFRESH_RATE)).isEqualTo(null);
+        mObserver.onDisplayAdded(EXTERNAL_DISPLAY);
+        assertThat(getVote(GLOBAL_ID, PRIORITY_SYNCHRONIZED_REFRESH_RATE)).isEqualTo(null);
+    }
+
+    private void init() {
+        mInjector = mock(DisplayModeDirector.Injector.class);
+        doAnswer(invocation -> {
+            assertThat(mObserver).isNull();
+            mObserver = invocation.getArgument(0);
+            return null;
+        }).when(mInjector).registerDisplayListener(any(), any());
+
+        doAnswer(c -> {
+            DisplayInfo info = c.getArgument(1);
+            info.type = Display.TYPE_INTERNAL;
+            info.displayId = DEFAULT_DISPLAY;
+            info.defaultModeId = 0;
+            info.supportedModes = mInternalDisplayModes;
+            info.userPreferredModeId = mInternalDisplayUserPreferredModeId;
+            return true;
+        }).when(mInjector).getDisplayInfo(eq(DEFAULT_DISPLAY), /*displayInfo=*/ any());
+
+        doAnswer(c -> {
+            DisplayInfo info = c.getArgument(1);
+            info.type = Display.TYPE_EXTERNAL;
+            info.displayId = EXTERNAL_DISPLAY;
+            info.defaultModeId = 0;
+            info.supportedModes = mExternalDisplayModes;
+            info.userPreferredModeId = mExternalDisplayUserPreferredModeId;
+            return true;
+        }).when(mInjector).getDisplayInfo(eq(EXTERNAL_DISPLAY), /*displayInfo=*/ any());
+
+        doAnswer(c -> mock(SensorManagerInternal.class)).when(mInjector).getSensorManagerInternal();
+        doAnswer(c -> mock(DeviceConfigInterface.class)).when(mInjector).getDeviceConfig();
+
+        mDefaultDisplay = mock(Display.class);
+        when(mDefaultDisplay.getDisplayId()).thenReturn(DEFAULT_DISPLAY);
+        doAnswer(c -> mInjector.getDisplayInfo(DEFAULT_DISPLAY, c.getArgument(0)))
+                .when(mDefaultDisplay).getDisplayInfo(/*displayInfo=*/ any());
+
+        mExternalDisplay = mock(Display.class);
+        when(mExternalDisplay.getDisplayId()).thenReturn(EXTERNAL_DISPLAY);
+        doAnswer(c -> mInjector.getDisplayInfo(EXTERNAL_DISPLAY, c.getArgument(0)))
+                .when(mExternalDisplay).getDisplayInfo(/*displayInfo=*/ any());
+
+        when(mInjector.getDisplays()).thenReturn(new Display[] {mDefaultDisplay, mExternalDisplay});
+
+        mDmd = new DisplayModeDirector(mContext, mHandler, mInjector, mDisplayManagerFlags);
+        mDmd.start(null);
+        assertThat(mObserver).isNotNull();
+    }
+
+    @Nullable
+    private Vote getVote(final int displayId, final int priority) {
+        return mDmd.getVote(displayId, priority);
+    }
+}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/VotesStorageTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/VotesStorageTest.java
index 287fdd5..50e2392 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/mode/VotesStorageTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/VotesStorageTest.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
 import android.util.SparseArray;
@@ -72,6 +73,18 @@
         verify(mVotesListener).onChanged();
     }
 
+    /** Verifies that adding the same vote twice results in a single call to onChanged */
+    @Test
+    public void notifiesVoteListenerCalledOnceIfVoteUpdatedTwice() {
+        // WHEN updateVote is called
+        mVotesStorage.updateVote(DISPLAY_ID, PRIORITY, VOTE);
+        mVotesStorage.updateVote(DISPLAY_ID, PRIORITY, VOTE);
+        mVotesStorage.updateVote(DISPLAY_ID, PRIORITY_OTHER, VOTE_OTHER);
+        mVotesStorage.updateVote(DISPLAY_ID, PRIORITY_OTHER, VOTE);
+        // THEN listener is notified, but only when vote changes.
+        verify(mVotesListener, times(3)).onChanged();
+    }
+
     @Test
     public void addsAnotherVoteToStorageWithDifferentPriority() {
         // GIVEN vote storage with one vote
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/AccessibilityServiceConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java
index 82b7540..2f0257a 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java
@@ -39,11 +39,16 @@
 import android.content.pm.ParceledListSlice;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
+import android.hardware.display.DisplayManager;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.DexmakerShareClassLoaderRule;
 import android.view.Display;
+import android.view.WindowManager;
 
 import com.android.server.accessibility.magnification.MagnificationProcessor;
 import com.android.server.accessibility.test.MessageCapturingHandler;
@@ -76,6 +81,9 @@
     public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
             new DexmakerShareClassLoaderRule();
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     AccessibilityServiceConnection mConnection;
 
     @Mock AccessibilityUserState mMockUserState;
@@ -113,6 +121,8 @@
 
         when(mMockIBinder.queryLocalInterface(any())).thenReturn(mMockServiceClient);
         when(mMockA11yTrace.isA11yTracingEnabled()).thenReturn(false);
+        when(mMockContext.getSystemService(Context.DISPLAY_SERVICE))
+                .thenReturn(new DisplayManager(mMockContext));
 
         mConnection = new AccessibilityServiceConnection(mMockUserState, mMockContext,
                 COMPONENT_NAME, mMockServiceInfo, SERVICE_ID, mHandler, new Object(),
@@ -168,6 +178,18 @@
         assertFalse(mConnection.getServiceInfo().crashed);
     }
 
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ADD_WINDOW_TOKEN_WITHOUT_LOCK)
+    public void onServiceConnected_addsWindowTokens() {
+        setServiceBinding(COMPONENT_NAME);
+        mConnection.bindLocked();
+        mConnection.onServiceConnected(COMPONENT_NAME, mMockIBinder);
+
+        verify(mMockWindowManagerInternal).addWindowToken(
+                any(), eq(WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY),
+                anyInt(), eq(null));
+    }
+
     private void setServiceBinding(ComponentName componentName) {
         when(mMockUserState.getBindingServicesLocked())
                 .thenReturn(new HashSet<>(Arrays.asList(componentName)));
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java
index b4558b2..63281b7 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java
@@ -35,6 +35,7 @@
 import static junit.framework.Assert.assertNull;
 import static junit.framework.Assert.assertTrue;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -46,6 +47,9 @@
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.graphics.Color;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.provider.Settings;
 import android.test.mock.MockContentResolver;
 import android.testing.DexmakerShareClassLoaderRule;
@@ -88,6 +92,9 @@
     @Rule public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
             new DexmakerShareClassLoaderRule();
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Mock private AccessibilityServiceInfo mMockServiceInfo;
 
     @Mock private AccessibilityServiceConnection mMockConnection;
@@ -188,7 +195,7 @@
 
         mUserState.addServiceLocked(mMockConnection);
 
-        verify(mMockConnection, never()).onAdded();
+        verify(mMockListener, never()).onServiceInfoChangedLocked(any());
     }
 
     @Test
@@ -197,13 +204,24 @@
 
         mUserState.addServiceLocked(mMockConnection);
 
-        verify(mMockConnection).onAdded();
         assertTrue(mUserState.getBoundServicesLocked().contains(mMockConnection));
         assertEquals(mMockConnection, mUserState.mComponentNameToServiceMap.get(COMPONENT_NAME));
         verify(mMockListener).onServiceInfoChangedLocked(eq(mUserState));
     }
 
     @Test
+    // addServiceLocked only calls addWindowTokensForAllDisplays when
+    // FLAG_ADD_WINDOW_TOKEN_WITHOUT_LOCK is off, so skip the test if it is on.
+    @RequiresFlagsDisabled(Flags.FLAG_ADD_WINDOW_TOKEN_WITHOUT_LOCK)
+    public void addService_flagDisabled_addsWindowTokens() {
+        when(mMockConnection.getComponentName()).thenReturn(COMPONENT_NAME);
+
+        mUserState.addServiceLocked(mMockConnection);
+
+        verify(mMockConnection).addWindowTokensForAllDisplays();
+    }
+
+    @Test
     public void reconcileSoftKeyboardMode_whenStateNotMatchSettings_setBothDefault() {
         // When soft kb show mode is hidden in settings and is auto in state.
         putSecureIntForUser(Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE,
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/UiAutomationManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/UiAutomationManagerTest.java
index 4ce9ba0..3ee5f61 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/UiAutomationManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/UiAutomationManagerTest.java
@@ -21,8 +21,11 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -36,6 +39,10 @@
 import android.content.pm.ServiceInfo;
 import android.hardware.display.DisplayManager;
 import android.os.IBinder;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.view.WindowManager;
 import android.view.accessibility.AccessibilityEvent;
 
 import com.android.server.accessibility.test.MessageCapturingHandler;
@@ -43,6 +50,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
@@ -58,6 +66,9 @@
 
     MessageCapturingHandler mMessageCapturingHandler;
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Mock Context mMockContext;
     @Mock AccessibilityServiceInfo mMockServiceInfo;
     @Mock ResolveInfo mMockResolveInfo;
@@ -197,6 +208,24 @@
         assertEquals(0, mUiAutomationManager.getRequestedEventMaskLocked());
     }
 
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ADD_WINDOW_TOKEN_WITHOUT_LOCK)
+    public void registerUiAutomationService_callsAddWindowTokenUsingHandler() {
+        register(0);
+        // registerUiTestAutomationServiceLocked() should not directly call addWindowToken.
+        verify(mMockWindowManagerInternal, never()).addWindowToken(
+                any(), anyInt(), anyInt(), any());
+
+        // Advance UiAutomationManager#UiAutomationService's handler.
+        mMessageCapturingHandler.sendAllMessages();
+
+        // After advancing the handler we expect addWindowToken to have been called
+        // by the UiAutomationService instance.
+        verify(mMockWindowManagerInternal).addWindowToken(
+                any(), eq(WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY),
+                anyInt(), eq(null));
+    }
+
     private void register(int flags) {
         mUiAutomationManager.registerUiTestAutomationServiceLocked(mMockOwner,
                 mMockAccessibilityServiceClient, mMockContext, mMockServiceInfo, SERVICE_ID,
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
index 349a597..430f600 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
@@ -687,7 +687,7 @@
         swipeAndHold(initCoords, edgeCoords);
 
         assertTrue(mMgh.mCurrentState == mMgh.mSinglePanningState);
-        assertTrue(mMgh.mSinglePanningState.mOverscrollState == mMgh.OVERSCROLL_LEFT_EDGE);
+        assertTrue(mMgh.mOverscrollHandler.mOverscrollState == mMgh.OVERSCROLL_LEFT_EDGE);
         assertTrue(isZoomed());
     }
 
@@ -711,7 +711,7 @@
         swipeAndHold(initCoords, edgeCoords);
 
         assertTrue(mMgh.mCurrentState == mMgh.mSinglePanningState);
-        assertTrue(mMgh.mSinglePanningState.mOverscrollState == mMgh.OVERSCROLL_RIGHT_EDGE);
+        assertTrue(mMgh.mOverscrollHandler.mOverscrollState == mMgh.OVERSCROLL_RIGHT_EDGE);
         assertTrue(isZoomed());
     }
 
@@ -734,7 +734,7 @@
 
         swipeAndHold(initCoords, edgeCoords);
 
-        assertTrue(mMgh.mSinglePanningState.mOverscrollState == mMgh.OVERSCROLL_VERTICAL_EDGE);
+        assertTrue(mMgh.mOverscrollHandler.mOverscrollState == mMgh.OVERSCROLL_VERTICAL_EDGE);
         assertTrue(isZoomed());
     }
 
@@ -756,7 +756,7 @@
 
         swipeAndHold(initCoords, edgeCoords);
 
-        assertTrue(mMgh.mSinglePanningState.mOverscrollState == mMgh.OVERSCROLL_NONE);
+        assertTrue(mMgh.mOverscrollHandler.mOverscrollState == mMgh.OVERSCROLL_NONE);
         assertTrue(isZoomed());
     }
 
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/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index 020afdb..8d7b5cb 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -141,6 +141,7 @@
 import com.android.server.notification.PermissionHelper.PackagePermission;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.protobuf.InvalidProtocolBufferException;
 
 import org.json.JSONArray;
@@ -563,7 +564,7 @@
                 mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2.getId(), false));
 
         List<NotificationChannelGroup> actualGroups = mHelper.getNotificationChannelGroups(
-                PKG_N_MR1, UID_N_MR1, false, true, false).getList();
+                PKG_N_MR1, UID_N_MR1, false, true, false, true, null).getList();
         boolean foundNcg = false;
         for (NotificationChannelGroup actual : actualGroups) {
             if (ncg.getId().equals(actual.getId())) {
@@ -647,7 +648,7 @@
                 mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3.getId(), false));
 
         List<NotificationChannelGroup> actualGroups = mHelper.getNotificationChannelGroups(
-                PKG_N_MR1, UID_N_MR1, false, true, false).getList();
+                PKG_N_MR1, UID_N_MR1, false, true, false, true, null).getList();
         boolean foundNcg = false;
         for (NotificationChannelGroup actual : actualGroups) {
             if (ncg.getId().equals(actual.getId())) {
@@ -2620,6 +2621,16 @@
     }
 
     @Test
+    public void testOnlyHasDefaultChannel() throws Exception {
+        assertTrue(mHelper.onlyHasDefaultChannel(PKG_N_MR1, UID_N_MR1));
+        assertFalse(mHelper.onlyHasDefaultChannel(PKG_O, UID_O));
+
+        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, getChannel(), true, false,
+                UID_N_MR1, false);
+        assertFalse(mHelper.onlyHasDefaultChannel(PKG_N_MR1, UID_N_MR1));
+    }
+
+    @Test
     public void testCreateDeletedChannel() throws Exception {
         long[] vibration = new long[]{100, 67, 145, 156};
         NotificationChannel channel =
@@ -2644,16 +2655,6 @@
     }
 
     @Test
-    public void testOnlyHasDefaultChannel() throws Exception {
-        assertTrue(mHelper.onlyHasDefaultChannel(PKG_N_MR1, UID_N_MR1));
-        assertFalse(mHelper.onlyHasDefaultChannel(PKG_O, UID_O));
-
-        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, getChannel(), true, false,
-                UID_N_MR1, false);
-        assertFalse(mHelper.onlyHasDefaultChannel(PKG_N_MR1, UID_N_MR1));
-    }
-
-    @Test
     public void testCreateChannel_defaultChannelId() throws Exception {
         try {
             mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, new NotificationChannel(
@@ -2884,7 +2885,7 @@
                 UID_N_MR1});
 
         assertEquals(0, mHelper.getNotificationChannelGroups(
-                PKG_N_MR1, UID_N_MR1, true, true, false).getList().size());
+                PKG_N_MR1, UID_N_MR1, true, true, false, true, null).getList().size());
     }
 
     @Test
@@ -3022,7 +3023,7 @@
                 UID_N_MR1, false);
 
         List<NotificationChannelGroup> actual = mHelper.getNotificationChannelGroups(
-                PKG_N_MR1, UID_N_MR1, true, true, false).getList();
+                PKG_N_MR1, UID_N_MR1, true, true, false, true, null).getList();
         assertEquals(3, actual.size());
         for (NotificationChannelGroup group : actual) {
             if (group.getId() == null) {
@@ -3056,14 +3057,15 @@
         channel1.setGroup(ncg.getId());
         mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false,
                 UID_N_MR1, false);
-        mHelper.getNotificationChannelGroups(PKG_N_MR1, UID_N_MR1, true, true, false).getList();
+        mHelper.getNotificationChannelGroups(PKG_N_MR1, UID_N_MR1, true, true, false, true, null)
+                .getList();
 
         channel1.setImportance(IMPORTANCE_LOW);
         mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true,
                 UID_N_MR1, false);
 
         List<NotificationChannelGroup> actual = mHelper.getNotificationChannelGroups(
-                PKG_N_MR1, UID_N_MR1, true, true, false).getList();
+                PKG_N_MR1, UID_N_MR1, true, true, false, true, null).getList();
 
         assertEquals(2, actual.size());
         for (NotificationChannelGroup group : actual) {
@@ -3089,7 +3091,7 @@
                 UID_N_MR1, false);
 
         List<NotificationChannelGroup> actual = mHelper.getNotificationChannelGroups(
-                PKG_N_MR1, UID_N_MR1, false, false, true).getList();
+                PKG_N_MR1, UID_N_MR1, false, false, true, true, null).getList();
 
         assertEquals(2, actual.size());
         for (NotificationChannelGroup group : actual) {
@@ -5786,6 +5788,62 @@
         assertFalse(isUserSet);
     }
 
+    @Test
+    public void testGetNotificationChannelGroups_withChannelFilter_includeBlocked() {
+        NotificationChannel channel =
+                new NotificationChannel("id2", "name1", NotificationManager.IMPORTANCE_HIGH);
+        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false,
+                UID_N_MR1, false);
+        // modifying same object, don't need to call updateNotificationChannel
+        channel.setImportance(IMPORTANCE_NONE);
+
+        NotificationChannel channel2 =
+                new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_HIGH);
+        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, true, false,
+                UID_N_MR1, false);
+
+        NotificationChannel channel3 =
+                new NotificationChannel("id3", "name3", NotificationManager.IMPORTANCE_HIGH);
+        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3, true, false,
+                UID_N_MR1, false);
+
+        Set<String> filter = ImmutableSet.of("id3");
+
+        NotificationChannelGroup actual = mHelper.getNotificationChannelGroups(
+                PKG_N_MR1, UID_N_MR1, false, true, false, true, filter).getList().get(0);
+        assertEquals(2, actual.getChannels().size());
+        assertEquals(1, actual.getChannels().stream().filter(c -> c.getId().equals("id3")).count());
+        assertEquals(1, actual.getChannels().stream().filter(c -> c.getId().equals("id2")).count());
+    }
+
+    @Test
+    public void testGetNotificationChannelGroups_withChannelFilter_doNotIncludeBlocked() {
+        NotificationChannel channel =
+                new NotificationChannel("id2", "name1", NotificationManager.IMPORTANCE_HIGH);
+        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false,
+                UID_N_MR1, false);
+        // modifying same object, don't need to call updateNotificationChannel
+        channel.setImportance(IMPORTANCE_NONE);
+
+        NotificationChannel channel2 =
+                new NotificationChannel("id2", "name2", NotificationManager.IMPORTANCE_HIGH);
+        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, true, false,
+                UID_N_MR1, false);
+
+        NotificationChannel channel3 =
+                new NotificationChannel("id3", "name3", NotificationManager.IMPORTANCE_HIGH);
+        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3, true, false,
+                UID_N_MR1, false);
+
+        Set<String> filter = ImmutableSet.of("id3");
+
+        NotificationChannelGroup actual = mHelper.getNotificationChannelGroups(
+                PKG_N_MR1, UID_N_MR1, false, true, false, false, filter).getList().get(0);
+        assertEquals(1, actual.getChannels().size());
+        assertEquals(1, actual.getChannels().stream().filter(c -> c.getId().equals("id3")).count());
+        assertEquals(0, actual.getChannels().stream().filter(c -> c.getId().equals("id2")).count());
+    }
+
     private static NotificationChannel cloneChannel(NotificationChannel original) {
         Parcel parcel = Parcel.obtain();
         try {
diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
index 61c4d06..8db09f9 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
@@ -46,6 +46,7 @@
 import static java.util.Collections.unmodifiableMap;
 
 import android.content.Context;
+import android.os.Looper;
 import android.os.SystemClock;
 import android.util.ArrayMap;
 import android.view.InputDevice;
@@ -98,6 +99,10 @@
      *      settings values.
      */
     protected final void setUpPhoneWindowManager(boolean supportSettingsUpdate) {
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+
         doReturn(mSettingsProviderRule.mockContentResolver(mContext))
                 .when(mContext).getContentResolver();
         mPhoneWindowManager = new TestPhoneWindowManager(mContext, supportSettingsUpdate);
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index 6e2c4bd..ef28ffa 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -70,6 +70,7 @@
 import android.hardware.input.InputManager;
 import android.media.AudioManagerInternal;
 import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.PowerManager;
 import android.os.PowerManagerInternal;
@@ -77,7 +78,6 @@
 import android.os.UserHandle;
 import android.os.Vibrator;
 import android.os.VibratorInfo;
-import android.os.test.TestLooper;
 import android.service.dreams.DreamManagerInternal;
 import android.telecom.TelecomManager;
 import android.util.FeatureFlagUtils;
@@ -160,8 +160,8 @@
     @Mock private KeyguardServiceDelegate mKeyguardServiceDelegate;
 
     private StaticMockitoSession mMockitoSession;
+    private HandlerThread mHandlerThread;
     private Handler mHandler;
-    private TestLooper mTestLooper;
 
     private class TestInjector extends PhoneWindowManager.Injector {
         TestInjector(Context context, WindowManagerPolicy.WindowManagerFuncs funcs) {
@@ -184,11 +184,12 @@
 
     TestPhoneWindowManager(Context context, boolean supportSettingsUpdate) {
         MockitoAnnotations.initMocks(this);
-        mTestLooper = new TestLooper();
-        mHandler = new Handler(mTestLooper.getLooper());
+        mHandlerThread = new HandlerThread("fake window manager");
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
         mContext = mockingDetails(context).isSpy() ? context : spy(context);
-        mHandler.post(() -> setUp(supportSettingsUpdate));
-        mTestLooper.dispatchAll();
+        mHandler.runWithScissors(() -> setUp(supportSettingsUpdate),  0 /* timeout */);
+        waitForIdle();
     }
 
     private void setUp(boolean supportSettingsUpdate) {
@@ -300,6 +301,7 @@
     }
 
     void tearDown() {
+        mHandlerThread.quitSafely();
         LocalServices.removeServiceForTest(InputMethodManagerInternal.class);
         Mockito.reset(mPhoneWindowManager);
         mMockitoSession.finishMocking();
@@ -326,7 +328,7 @@
     }
 
     void waitForIdle() {
-        mTestLooper.dispatchAll();
+        mHandler.runWithScissors(() -> { }, 0 /* timeout */);
     }
 
     /**
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 0494dfd..f65cb93 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -147,14 +147,12 @@
 import android.view.IRemoteAnimationFinishedCallback;
 import android.view.IRemoteAnimationRunner.Stub;
 import android.view.IWindowManager;
-import android.view.IWindowSession;
 import android.view.InsetsSource;
 import android.view.InsetsState;
 import android.view.RemoteAnimationAdapter;
 import android.view.RemoteAnimationTarget;
 import android.view.Surface;
 import android.view.WindowManager;
-import android.view.WindowManagerGlobal;
 import android.window.TaskSnapshot;
 
 import androidx.test.filters.MediumTest;
@@ -2073,7 +2071,7 @@
                 WindowManager.LayoutParams.TYPE_APPLICATION_STARTING);
         params.width = params.height = WindowManager.LayoutParams.MATCH_PARENT;
         final TestWindowState w = new TestWindowState(
-                mAtm.mWindowManager, mock(Session.class), new TestIWindow(), params, activity);
+                mAtm.mWindowManager, getTestSession(), new TestIWindow(), params, activity);
         activity.addWindow(w);
 
         // Assume the activity is launching in different rotation, and there was an available
@@ -2083,23 +2081,8 @@
                 .build();
         setRotatedScreenOrientationSilently(activity);
         activity.setVisible(false);
-
-        final IWindowSession session = WindowManagerGlobal.getWindowSession();
-        spyOn(session);
-        try {
-            // Return error to skip unnecessary operation.
-            doReturn(WindowManagerGlobal.ADD_STARTING_NOT_NEEDED).when(session).addToDisplay(
-                    any() /* window */,  any() /* attrs */,
-                    anyInt() /* viewVisibility */, anyInt() /* displayId */,
-                    anyInt() /* requestedVisibleTypes */, any() /* outInputChannel */,
-                    any() /* outInsetsState */, any() /* outActiveControls */,
-                    any() /* outAttachedFrame */, any() /* outSizeCompatScale */);
-            mAtm.mWindowManager.mStartingSurfaceController
-                    .createTaskSnapshotSurface(activity, snapshot);
-        } catch (RemoteException ignored) {
-        } finally {
-            reset(session);
-        }
+        mAtm.mWindowManager.mStartingSurfaceController
+                .createTaskSnapshotSurface(activity, snapshot);
 
         // Because the rotation of snapshot and the corresponding top activity are different, fixed
         // rotation should be applied when creating snapshot surface if the display rotation may be
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index f30ecbe..8e7ba70 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
@@ -337,6 +337,7 @@
         WindowState appWindow = task.getTopVisibleAppMainWindow();
         WindowOnBackInvokedDispatcher dispatcher =
                 new WindowOnBackInvokedDispatcher(context);
+        spyOn(appWindow.mSession);
         doAnswer(invocation -> {
             appWindow.setOnBackInvokedCallbackInfo(invocation.getArgument(1));
             return null;
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayAreaTest.java b/services/tests/wmtests/src/com/android/server/wm/DisplayAreaTest.java
index 1ad04a2..cd0389d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayAreaTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayAreaTest.java
@@ -700,7 +700,7 @@
     }
 
     private WindowState createWindowState(WindowToken token) {
-        return new WindowState(mWm, mock(Session.class), new TestIWindow(), token,
+        return new WindowState(mWm, getTestSession(), new TestIWindow(), token,
                 null /* parentWindow */, 0 /* appOp */, new WindowManager.LayoutParams(),
                 View.VISIBLE, 0 /* ownerId */, 0 /* showUserId */,
                 false /* ownerCanAddInternalSystemWindow */);
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index ae4ebc1..c2b7fec 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -573,7 +573,10 @@
         assertEquals(window1, mWm.mRoot.getTopFocusedDisplayContent().mCurrentFocus);
 
         // Add a window to the second display, and it should be focused
-        final WindowState window2 = createWindow(null, TYPE_BASE_APPLICATION, dc, "window2");
+        final ActivityRecord app2 = new ActivityBuilder(mAtm)
+                .setTask(new TaskBuilder(mSupervisor).setDisplay(dc).build())
+                .setUseProcess(window1.getProcess()).setOnTop(true).build();
+        final WindowState window2 = createWindow(null, TYPE_BASE_APPLICATION, app2, "window2");
         window2.mActivityRecord.mTargetSdk = targetSdk;
         updateFocusedWindow();
         assertTrue(window2.isFocused());
@@ -1088,7 +1091,7 @@
 
         assertFalse(dc.getRotationReversionController().isAnyOverrideActive());
         dc.getDisplayRotation().setUserRotation(WindowManagerPolicy.USER_ROTATION_LOCKED,
-                ROTATION_90);
+                ROTATION_90, /* caller= */ "DisplayContentTests");
         updateAllDisplayContentAndRotation(dc);
         assertEquals(ROTATION_90, dc.getDisplayRotation()
                 .rotationForOrientation(SCREEN_ORIENTATION_UNSPECIFIED, ROTATION_90));
@@ -1107,7 +1110,7 @@
         assertEquals(ROTATION_90, dc.getDisplayRotation()
                 .rotationForOrientation(SCREEN_ORIENTATION_UNSPECIFIED, ROTATION_0));
         dc.getDisplayRotation().setUserRotation(WindowManagerPolicy.USER_ROTATION_FREE,
-                ROTATION_0);
+                ROTATION_0, /* caller= */ "DisplayContentTests");
     }
 
     @Test
@@ -1134,7 +1137,8 @@
         dc.getDisplayRotation().setFixedToUserRotation(
                 IWindowManager.FIXED_TO_USER_ROTATION_ENABLED);
         dc.getDisplayRotation().setUserRotation(
-                WindowManagerPolicy.USER_ROTATION_LOCKED, ROTATION_180);
+                WindowManagerPolicy.USER_ROTATION_LOCKED, ROTATION_180,
+                /* caller= */ "DisplayContentTests");
         final int newOrientation = getRotatedOrientation(dc);
 
         final Task task = new TaskBuilder(mSupervisor)
@@ -1174,7 +1178,8 @@
         dc.getDisplayRotation().setFixedToUserRotation(
                 IWindowManager.FIXED_TO_USER_ROTATION_ENABLED);
         dc.getDisplayRotation().setUserRotation(
-                WindowManagerPolicy.USER_ROTATION_LOCKED, ROTATION_0);
+                WindowManagerPolicy.USER_ROTATION_LOCKED, ROTATION_0,
+                /* caller= */ "DisplayContentTests");
         dc.getDefaultTaskDisplayArea().setWindowingMode(WINDOWING_MODE_FULLSCREEN);
         final int newOrientation = getRotatedOrientation(dc);
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
index 915b387..e14568d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
@@ -1265,7 +1265,7 @@
     }
 
     private void freezeRotation(int rotation) {
-        mTarget.freezeRotation(rotation);
+        mTarget.freezeRotation(rotation, /* caller= */ "DisplayRotationTests");
 
         if (mTarget.isDefaultDisplay) {
             mAccelerometerRotationObserver.onChange(false);
@@ -1274,7 +1274,7 @@
     }
 
     private void thawRotation() {
-        mTarget.thawRotation();
+        mTarget.thawRotation(/* caller= */ "DisplayRotationTests");
 
         if (mTarget.isDefaultDisplay) {
             mAccelerometerRotationObserver.onChange(false);
diff --git a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
index 4526d18..50fe042 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
@@ -55,7 +55,6 @@
 import android.os.UserHandle;
 import android.platform.test.annotations.Presubmit;
 import android.view.DragEvent;
-import android.view.IWindowSessionCallback;
 import android.view.InputChannel;
 import android.view.SurfaceControl;
 import android.view.SurfaceSession;
@@ -98,6 +97,7 @@
     private static final String TEST_PACKAGE = "com.test.package";
 
     private TestDragDropController mTarget;
+    private WindowProcessController mProcess;
     private WindowState mWindow;
     private IBinder mToken;
 
@@ -137,10 +137,9 @@
      * Creates a window state which can be used as a drop target.
      */
     private WindowState createDropTargetWindow(String name, int ownerId) {
-        final ActivityRecord activity = createNonAttachedActivityRecord(mDisplayContent);
-        final Task rootTask = createTask(mDisplayContent);
-        final Task task = createTaskInRootTask(rootTask, ownerId);
-        task.addChild(activity, 0);
+        final Task task = new TaskBuilder(mSupervisor).setUserId(ownerId).build();
+        final ActivityRecord activity = new ActivityBuilder(mAtm).setTask(task)
+                .setUseProcess(mProcess).build();
 
         // Use a new TestIWindow so we don't collect events for other windows
         final WindowState window = createWindow(
@@ -167,6 +166,8 @@
     @Before
     public void setUp() throws Exception {
         mTarget = new TestDragDropController(mWm, mWm.mH.getLooper());
+        mProcess = mSystemServicesTestRule.addProcess(TEST_PACKAGE, "testProc",
+                TEST_PID, TEST_UID);
         mWindow = createDropTargetWindow("Drag test window", 0);
         doReturn(mWindow).when(mDisplayContent).getTouchableWinAtPointLocked(0, 0);
         when(mWm.mInputManager.transferTouchFocus(any(InputChannel.class),
@@ -221,8 +222,6 @@
 
     @Test
     public void testPrivateInterceptGlobalDragDropFlagChecksPermission() {
-        spyOn(mWm.mContext);
-
         DisplayPolicy policy = mDisplayContent.getDisplayPolicy();
         WindowManager.LayoutParams attrs = new WindowManager.LayoutParams();
         attrs.privateFlags |= PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP;
@@ -323,10 +322,7 @@
 
     @Test
     public void testValidateAppActivityArguments() {
-        final Session session = new Session(mWm, new IWindowSessionCallback.Stub() {
-            @Override
-            public void onAnimatorScaleChanged(float scale) {}
-        });
+        final Session session = getTestSession();
         try {
             session.validateAndResolveDragMimeTypeExtras(
                     createClipDataForActivity(mock(PendingIntent.class), null), TEST_UID, TEST_PID,
@@ -364,10 +360,7 @@
     public void testValidateAppShortcutArguments() {
         doReturn(PERMISSION_GRANTED).when(mWm.mContext)
                 .checkCallingOrSelfPermission(eq(START_TASKS_FROM_RECENTS));
-        final Session session = new Session(mWm, new IWindowSessionCallback.Stub() {
-            @Override
-            public void onAnimatorScaleChanged(float scale) {}
-        });
+        final Session session = createTestSession(mAtm);
         try {
             session.validateAndResolveDragMimeTypeExtras(
                     createClipDataForShortcut(null, "test_shortcut_id", mock(UserHandle.class)),
@@ -398,10 +391,7 @@
     public void testValidateProfileAppShortcutArguments_notCallingUid() {
         doReturn(PERMISSION_GRANTED).when(mWm.mContext)
                 .checkCallingOrSelfPermission(eq(START_TASKS_FROM_RECENTS));
-        final Session session = Mockito.spy(new Session(mWm, new IWindowSessionCallback.Stub() {
-            @Override
-            public void onAnimatorScaleChanged(float scale) {}
-        }));
+        final Session session = createTestSession(mAtm);
         final ShortcutServiceInternal shortcutService = mock(ShortcutServiceInternal.class);
         final Intent[] shortcutIntents = new Intent[1];
         shortcutIntents[0] = new Intent();
@@ -443,10 +433,7 @@
     public void testValidateAppTaskArguments() {
         doReturn(PERMISSION_GRANTED).when(mWm.mContext)
                 .checkCallingOrSelfPermission(eq(START_TASKS_FROM_RECENTS));
-        final Session session = new Session(mWm, new IWindowSessionCallback.Stub() {
-            @Override
-            public void onAnimatorScaleChanged(float scale) {}
-        });
+        final Session session = createTestSession(mAtm);
         try {
             final ClipData clipData = new ClipData(
                     new ClipDescription("drag", new String[] { MIMETYPE_APPLICATION_TASK }),
@@ -462,10 +449,7 @@
 
     @Test
     public void testValidateFlags() {
-        final Session session = new Session(mWm, new IWindowSessionCallback.Stub() {
-            @Override
-            public void onAnimatorScaleChanged(float scale) {}
-        });
+        final Session session = getTestSession();
         try {
             session.validateDragFlags(View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION);
             fail("Expected failure without permission");
@@ -478,10 +462,7 @@
     public void testValidateFlagsWithPermission() {
         doReturn(PERMISSION_GRANTED).when(mWm.mContext)
                 .checkCallingOrSelfPermission(eq(START_TASKS_FROM_RECENTS));
-        final Session session = new Session(mWm, new IWindowSessionCallback.Stub() {
-            @Override
-            public void onAnimatorScaleChanged(float scale) {}
-        });
+        final Session session = createTestSession(mAtm);
         try {
             session.validateDragFlags(View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION);
             // Expected pass
@@ -571,7 +552,8 @@
 
             assertTrue(mWm.mInputManager.transferTouchFocus(new InputChannel(),
                     new InputChannel(), true /* isDragDrop */));
-            mToken = mTarget.performDrag(0, 0, mWindow.mClient, flag, surface, 0, 0, 0, 0, 0, data);
+            mToken = mTarget.performDrag(TEST_PID, 0, mWindow.mClient,
+                    flag, surface, 0, 0, 0, 0, 0, data);
             assertNotNull(mToken);
 
             r.run();
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 8f68c0f..1ceb1a8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -4739,24 +4739,20 @@
         assertEquals(new Rect(1050, 0, 1750, 1400), mActivity.getBounds());
     }
 
-    private static WindowState addWindowToActivity(ActivityRecord activity) {
+    private WindowState addWindowToActivity(ActivityRecord activity) {
         final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
         params.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
         params.setFitInsetsSides(0);
         params.setFitInsetsTypes(0);
         final TestWindowState w = new TestWindowState(
-                activity.mWmService, mock(Session.class), new TestIWindow(), params, activity);
+                activity.mWmService, getTestSession(), new TestIWindow(), params, activity);
         makeWindowVisible(w);
         w.mWinAnimator.mDrawState = WindowStateAnimator.HAS_DRAWN;
         activity.addWindow(w);
         return w;
     }
 
-    private static TestWindowState addStatusBar(DisplayContent displayContent) {
-        final DisplayPolicy displayPolicy = displayContent.getDisplayPolicy();
-        doReturn(true).when(displayPolicy).hasStatusBar();
-        displayPolicy.onConfigurationChanged();
-
+    private TestWindowState addStatusBar(DisplayContent displayContent) {
         final TestWindowToken token = createTestWindowToken(
                 TYPE_STATUS_BAR, displayContent);
         final WindowManager.LayoutParams attrs =
@@ -4772,11 +4768,12 @@
                 new InsetsFrameProvider(owner, 0, WindowInsets.Type.mandatorySystemGestures())
         };
         final TestWindowState statusBar = new TestWindowState(
-                displayContent.mWmService, mock(Session.class), new TestIWindow(), attrs, token);
+                displayContent.mWmService, getTestSession(), new TestIWindow(), attrs, token);
         token.addWindow(statusBar);
         statusBar.setRequestedSize(displayContent.mBaseDisplayWidth,
                 SystemBarUtils.getStatusBarHeight(displayContent.getDisplayUiContext()));
 
+        final DisplayPolicy displayPolicy = displayContent.getDisplayPolicy();
         displayPolicy.addWindowLw(statusBar, attrs);
         displayPolicy.layoutWindowLw(statusBar, null, displayContent.mDisplayFrames);
         return statusBar;
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
index fb27d63..0c58069 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -610,7 +610,7 @@
         // display.
         final DisplayRotation dr = display.mDisplayContent.getDisplayRotation();
         dr.setFixedToUserRotation(FIXED_TO_USER_ROTATION_ENABLED);
-        dr.setUserRotation(USER_ROTATION_FREE, ROTATION_0);
+        dr.setUserRotation(USER_ROTATION_FREE, ROTATION_0, /* caller= */ "TaskTests");
 
         final Task rootTask = new TaskBuilder(mSupervisor).setCreateActivity(true)
                 .setWindowingMode(WINDOWING_MODE_FULLSCREEN).setDisplay(display).build();
@@ -642,7 +642,7 @@
         // Setting app to fixed landscape and changing display
         top.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE);
         // Fix the display orientation to portrait which is 90 degrees for the test display.
-        dr.setUserRotation(USER_ROTATION_FREE, ROTATION_90);
+        dr.setUserRotation(USER_ROTATION_FREE, ROTATION_90, /* caller= */ "TaskTests");
 
         // Fixed orientation request should be resolved on activity level. Task fills display
         // bounds.
@@ -681,7 +681,7 @@
         // display.
         final DisplayRotation dr = display.mDisplayContent.getDisplayRotation();
         dr.setFixedToUserRotation(FIXED_TO_USER_ROTATION_ENABLED);
-        dr.setUserRotation(USER_ROTATION_FREE, ROTATION_0);
+        dr.setUserRotation(USER_ROTATION_FREE, ROTATION_0, /* caller= */ "TaskTests");
 
         final Task rootTask = new TaskBuilder(mSupervisor).setCreateActivity(true)
                 .setWindowingMode(WINDOWING_MODE_FULLSCREEN).setDisplay(display).build();
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index 76576f7..e86fc36 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -82,7 +82,6 @@
 import android.util.MergedConfiguration;
 import android.view.ContentRecordingSession;
 import android.view.IWindow;
-import android.view.IWindowSessionCallback;
 import android.view.InputChannel;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
@@ -496,11 +495,7 @@
         spyOn(mWm.mWindowContextListenerController);
 
         final WindowToken windowToken = createTestWindowToken(TYPE_INPUT_METHOD, mDefaultDisplay);
-        final Session session = new Session(mWm, new IWindowSessionCallback.Stub() {
-            @Override
-            public void onAnimatorScaleChanged(float v) throws RemoteException {
-            }
-        });
+        final Session session = getTestSession();
         final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
                 TYPE_APPLICATION_ATTACHED_DIALOG);
         params.token = windowToken.token;
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
index 0ddd31355..014d57d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -95,7 +95,6 @@
 import android.util.MergedConfiguration;
 import android.view.Gravity;
 import android.view.IWindow;
-import android.view.IWindowSessionCallback;
 import android.view.InputWindowHandle;
 import android.view.InsetsSource;
 import android.view.InsetsSourceControl;
@@ -1332,12 +1331,7 @@
         final WindowToken windowToken = createTestWindowToken(TYPE_APPLICATION_OVERLAY,
                 mDisplayContent);
         final IWindow client = new TestIWindow();
-        final Session session = new Session(mWm, new IWindowSessionCallback.Stub() {
-            @Override
-            public void onAnimatorScaleChanged(float v) throws RemoteException {
-
-            }
-        });
+        final Session session = getTestSession();
         final ClientWindowFrames outFrames = new ClientWindowFrames();
         final MergedConfiguration outConfig = new MergedConfiguration();
         final SurfaceControl outSurfaceControl = new SurfaceControl();
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index 99688da..9146889 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -94,6 +94,7 @@
 import android.view.Gravity;
 import android.view.IDisplayWindowInsetsController;
 import android.view.IWindow;
+import android.view.IWindowSessionCallback;
 import android.view.InsetsFrameProvider;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
@@ -153,7 +154,7 @@
     ActivityTaskSupervisor mSupervisor;
     WindowManagerService mWm;
     private final IWindow mIWindow = new TestIWindow();
-    private Session mMockSession;
+    private Session mTestSession;
     private boolean mUseFakeSettingsProvider;
 
     DisplayInfo mDisplayInfo = new DisplayInfo();
@@ -231,7 +232,6 @@
         suppressInsetsAnimation(insetsPolicy.getPermanentControlTarget());
 
         mTransaction = mSystemServicesTestRule.mTransaction;
-        mMockSession = mock(Session.class);
 
         mContext.getSystemService(DisplayManager.class)
                 .getDisplay(Display.DEFAULT_DISPLAY).getDisplayInfo(mDisplayInfo);
@@ -508,6 +508,54 @@
         return statusBar;
     }
 
+    Session getTestSession() {
+        if (mTestSession != null) {
+            return mTestSession;
+        }
+        mTestSession = createTestSession(mAtm);
+        return mTestSession;
+    }
+
+    private Session getTestSession(WindowToken token) {
+        final ActivityRecord r = token.asActivityRecord();
+        if (r == null || r.app == null) {
+            return getTestSession();
+        }
+        // If the activity has a process, let the window session belonging to activity use the
+        // process of the activity.
+        int pid = r.app.getPid();
+        if (pid == 0) {
+            // See SystemServicesTestRule#addProcess, pid 0 isn't added to the map. So generate
+            // a non-zero pid to initialize it.
+            final int numPid = mAtm.mProcessMap.getPidMap().size();
+            pid = numPid > 0 ? mAtm.mProcessMap.getPidMap().keyAt(numPid - 1) + 1 : 1;
+            r.app.setPid(pid);
+            mAtm.mProcessMap.put(pid, r.app);
+        } else {
+            final WindowState win = mRootWindowContainer.getWindow(w -> w.getProcess() == r.app);
+            if (win != null) {
+                // Reuse the same Session if there is a window uses the same process.
+                return win.mSession;
+            }
+        }
+        return createTestSession(mAtm, pid, r.getUid());
+    }
+
+    static Session createTestSession(ActivityTaskManagerService atms) {
+        return createTestSession(atms, WindowManagerService.MY_PID, WindowManagerService.MY_UID);
+    }
+
+    static Session createTestSession(ActivityTaskManagerService atms, int pid, int uid) {
+        if (atms.mProcessMap.getProcess(pid) == null) {
+            SystemServicesTestRule.addProcess(atms, "testPkg", "testProc", pid, uid);
+        }
+        return new Session(atms.mWindowManager, new IWindowSessionCallback.Stub() {
+            @Override
+            public void onAnimatorScaleChanged(float scale) {
+            }
+        }, pid, uid);
+    }
+
     WindowState createAppWindow(Task task, int type, String name) {
         final ActivityRecord activity = createNonAttachedActivityRecord(task.getDisplayContent());
         task.addChild(activity, 0);
@@ -587,7 +635,7 @@
     WindowState createWindow(WindowState parent, int type, WindowToken token, String name,
             int ownerId, boolean ownerCanAddInternalSystemWindow, IWindow iwindow) {
         return createWindow(parent, type, token, name, ownerId, UserHandle.getUserId(ownerId),
-                ownerCanAddInternalSystemWindow, mWm, mMockSession, iwindow,
+                ownerCanAddInternalSystemWindow, mWm, getTestSession(token), iwindow,
                 mSystemServicesTestRule.getPowerManagerWrapper());
     }
 
@@ -891,7 +939,7 @@
     TestWindowState createWindowState(WindowManager.LayoutParams attrs, WindowToken token) {
         SystemServicesTestRule.checkHoldsLock(mWm.mGlobalLock);
 
-        return new TestWindowState(mWm, mMockSession, mIWindow, attrs, token);
+        return new TestWindowState(mWm, getTestSession(), mIWindow, attrs, token);
     }
 
     /** Creates a {@link DisplayContent} as parts of simulate display info for test. */
@@ -1705,8 +1753,7 @@
                 final WindowState window = WindowTestsBase.createWindow(null,
                         TYPE_APPLICATION_STARTING, activity,
                         "Starting window", 0 /* ownerId */, 0 /* userId*/,
-                        false /* internalWindows */, mWMService, mock(Session.class),
-                        iWindow,
+                        false /* internalWindows */, mWMService, createTestSession(mAtm), iWindow,
                         mPowerManagerWrapper);
                 activity.mStartingWindow = window;
                 mAppWindowMap.put(info.appToken, window);
diff --git a/tests/BinderLeakTest/Android.bp b/tests/BinderLeakTest/Android.bp
new file mode 100644
index 0000000..78b0ede
--- /dev/null
+++ b/tests/BinderLeakTest/Android.bp
@@ -0,0 +1,40 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+    name: "binder_leak_test_aidl",
+    srcs: ["**/*.aidl"],
+    path: "aidl",
+}
+
+java_defaults {
+    name: "BinderTest.defaults",
+    srcs: [
+        "**/*.java",
+        ":binder_leak_test_aidl",
+    ],
+    static_libs: [
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+        "androidx.test.runner",
+    ],
+}
+
+// Built with target_sdk_version: current
+android_test {
+    name: "BinderLeakTest",
+    defaults: ["BinderTest.defaults"],
+}
+
+// Built with target_sdk_version: 33
+android_test {
+    name: "BinderLeakTest_legacy",
+    defaults: ["BinderTest.defaults"],
+    manifest: "AndroidManifest_legacy.xml",
+}
diff --git a/tests/BinderLeakTest/AndroidManifest.xml b/tests/BinderLeakTest/AndroidManifest.xml
new file mode 100644
index 0000000..756def7
--- /dev/null
+++ b/tests/BinderLeakTest/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.test.binder">
+    <application>
+        <service
+            android:name=".MyService"
+            android:enabled="true"
+            android:exported="true"
+            android:process=":service">
+        </service>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.test.binder"
+        android:label="Binder leak test">
+    </instrumentation>
+</manifest>
diff --git a/tests/BinderLeakTest/AndroidManifest_legacy.xml b/tests/BinderLeakTest/AndroidManifest_legacy.xml
new file mode 100644
index 0000000..03d1dfd
--- /dev/null
+++ b/tests/BinderLeakTest/AndroidManifest_legacy.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.test.binder">
+    <uses-sdk android:minSdkVersion="33"
+          android:targetSdkVersion="33"
+          android:maxSdkVersion="33" />
+    <application>
+        <service
+            android:name=".MyService"
+            android:enabled="true"
+            android:exported="true"
+            android:process=":service">
+        </service>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.test.binder"
+        android:label="Binder leak test">
+    </instrumentation>
+</manifest>
diff --git a/tests/BinderLeakTest/aidl/com/android/test/binder/IFoo.aidl b/tests/BinderLeakTest/aidl/com/android/test/binder/IFoo.aidl
new file mode 100644
index 0000000..a721959
--- /dev/null
+++ b/tests/BinderLeakTest/aidl/com/android/test/binder/IFoo.aidl
@@ -0,0 +1,5 @@
+package com.android.test.binder;
+
+interface IFoo {
+
+}
diff --git a/tests/BinderLeakTest/aidl/com/android/test/binder/IFooProvider.aidl b/tests/BinderLeakTest/aidl/com/android/test/binder/IFooProvider.aidl
new file mode 100644
index 0000000..b487f51
--- /dev/null
+++ b/tests/BinderLeakTest/aidl/com/android/test/binder/IFooProvider.aidl
@@ -0,0 +1,10 @@
+package com.android.test.binder;
+import com.android.test.binder.IFoo;
+
+interface IFooProvider {
+    IFoo createFoo();
+
+    boolean isFooGarbageCollected();
+
+    oneway void killProcess();
+}
diff --git a/tests/BinderLeakTest/java/com/android/test/binder/BinderTest.java b/tests/BinderLeakTest/java/com/android/test/binder/BinderTest.java
new file mode 100644
index 0000000..f07317f
--- /dev/null
+++ b/tests/BinderLeakTest/java/com/android/test/binder/BinderTest.java
@@ -0,0 +1,153 @@
+/*
+ * 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.test.binder;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Intent;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.rule.ServiceTestRule;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.concurrent.TimeoutException;
+
+@RunWith(AndroidJUnit4.class)
+public class BinderTest {
+    @Rule
+    public final ServiceTestRule serviceRule = new ServiceTestRule();
+
+    @Test
+    public void testDeathRecipientLeaksOrNot()
+            throws RemoteException, TimeoutException, InterruptedException {
+        Intent intent = new Intent(ApplicationProvider.getApplicationContext(), MyService.class);
+        IFooProvider provider = IFooProvider.Stub.asInterface(serviceRule.bindService(intent));
+        FooHolder holder = new FooHolder(provider.createFoo());
+
+        // ref will get enqueued right after holder is finalized for gc.
+        ReferenceQueue<FooHolder> refQueue = new ReferenceQueue<>();
+        PhantomReference<FooHolder> ref = new PhantomReference<>(holder, refQueue);
+
+        DeathRecorder deathRecorder = new DeathRecorder();
+        holder.registerDeathRecorder(deathRecorder);
+
+        if (getSdkVersion() >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+            /////////////////////////////////////////////
+            // New behavior
+            //
+            // Reference chain at this moment:
+            // holder --(java strong ref)--> FooHolder
+            // FooHolder.mProxy --(java strong ref)--> IFoo.Proxy
+            // IFoo.Proxy.mRemote --(java strong ref)--> BinderProxy
+            // BinderProxy --(binder ref)--> Foo.Stub
+            // In other words, the variable "holder" is the root of the reference chain.
+
+            // By setting the variable to null, we make FooHolder, IFoo.Proxy, BinderProxy, and even
+            // Foo.Stub unreachable.
+            holder = null;
+
+            // Ensure that the objects are garbage collected
+            forceGc();
+            assertEquals(ref, refQueue.poll());
+            assertTrue(provider.isFooGarbageCollected());
+
+            // The binder has died, but we don't get notified since the death recipient is GC'ed.
+            provider.killProcess();
+            Thread.sleep(1000); // give some time for the service process to die and reaped
+            assertFalse(deathRecorder.deathRecorded);
+        } else {
+            /////////////////////////////////////////////
+            // Legacy behavior
+            //
+            // Reference chain at this moment:
+            // JavaDeathRecipient --(JNI strong ref)--> FooHolder
+            // holder --(java strong ref)--> FooHolder
+            // FooHolder.mProxy --(java strong ref)--> IFoo.Proxy
+            // IFoo.Proxy.mRemote --(java strong ref)--> BinderProxy
+            // BinderProxy --(binder ref)--> Foo.Stub
+            // So, BOTH JavaDeathRecipient and holder are roots of the reference chain.
+
+            // Even if we set holder to null, it doesn't make other objects unreachable; they are
+            // still reachable via the JNI strong ref.
+            holder = null;
+
+            // Check that objects are not garbage collected
+            forceGc();
+            assertNotEquals(ref, refQueue.poll());
+            assertFalse(provider.isFooGarbageCollected());
+
+            // The legacy behavior is getting notified even when there's no reference
+            provider.killProcess();
+            Thread.sleep(1000); // give some time for the service process to die and reaped
+            assertTrue(deathRecorder.deathRecorded);
+        }
+    }
+
+    static class FooHolder implements IBinder.DeathRecipient {
+        private IFoo mProxy;
+        private DeathRecorder mDeathRecorder;
+
+        FooHolder(IFoo proxy) throws RemoteException {
+            proxy.asBinder().linkToDeath(this, 0);
+
+            // A strong reference from DeathRecipient(this) to the binder proxy is created here
+            mProxy = proxy;
+        }
+
+        public void registerDeathRecorder(DeathRecorder dr) {
+            mDeathRecorder = dr;
+        }
+
+        @Override
+        public void binderDied() {
+            if (mDeathRecorder != null) {
+                mDeathRecorder.deathRecorded = true;
+            }
+        }
+    }
+
+    static class DeathRecorder {
+        public boolean deathRecorded = false;
+    }
+
+    // Try calling System.gc() until an orphaned object is confirmed to be finalized
+    private static void forceGc() {
+        Object obj = new Object();
+        ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
+        PhantomReference<Object> ref = new PhantomReference<>(obj, refQueue);
+        obj = null; // make it an orphan
+        while (refQueue.poll() != ref) {
+            System.gc();
+        }
+    }
+
+    private static int getSdkVersion() {
+        return ApplicationProvider.getApplicationContext().getApplicationInfo().targetSdkVersion;
+    }
+}
diff --git a/tests/BinderLeakTest/java/com/android/test/binder/MyService.java b/tests/BinderLeakTest/java/com/android/test/binder/MyService.java
new file mode 100644
index 0000000..c701253
--- /dev/null
+++ b/tests/BinderLeakTest/java/com/android/test/binder/MyService.java
@@ -0,0 +1,63 @@
+/*
+ * 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.test.binder;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+
+public class MyService extends Service {
+    @Override
+    public IBinder onBind(Intent intent) {
+        return new IFooProvider.Stub() {
+            ReferenceQueue<IFoo> mRefQueue = new ReferenceQueue<>();
+            PhantomReference<IFoo> mRef;
+
+            @Override
+            public IFoo createFoo() throws RemoteException {
+                IFoo binder = new IFoo.Stub() {};
+                mRef = new PhantomReference<>(binder, mRefQueue);
+                return binder;
+            }
+
+            @Override
+            public boolean isFooGarbageCollected() throws RemoteException {
+                forceGc();
+                return mRefQueue.poll() == mRef;
+            }
+
+            @Override
+            public void killProcess() throws RemoteException {
+                android.os.Process.killProcess(android.os.Process.myPid());
+            }
+        };
+    }
+
+    private static void forceGc() {
+        Object obj = new Object();
+        ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
+        PhantomReference<Object> ref = new PhantomReference<>(obj, refQueue);
+        obj = null; // make it an orphan
+        while (refQueue.poll() != ref) {
+            System.gc();
+        }
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt
index ee22d07..badd7c8 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt
@@ -50,7 +50,6 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@FlakyTest(bugId = 298544839)
 class QuickSwitchBetweenTwoAppsForwardTest(flicker: LegacyFlickerTest) : BaseTest(flicker) {
     private val testApp1 = SimpleAppHelper(instrumentation)
     private val testApp2 = NonResizeableAppHelper(instrumentation)
@@ -92,6 +91,7 @@
      * Checks that the transition starts with [testApp1]'s windows filling/covering exactly the
      * entirety of the display.
      */
+    @FlakyTest(bugId = 298544839)
     @Test
     open fun startsWithApp1WindowsCoverFullScreen() {
         flicker.assertWmStart {
@@ -120,6 +120,7 @@
      * Checks that [testApp2] windows fill the entire screen (i.e. is "fullscreen") at the end of
      * the transition once we have fully quick switched from [testApp1] back to the [testApp2].
      */
+    @FlakyTest(bugId = 298544839)
     @Test
     open fun endsWithApp2WindowsCoveringFullScreen() {
         flicker.assertWmEnd { this.visibleRegion(testApp2).coversExactly(startDisplayBounds) }
@@ -129,6 +130,7 @@
      * Checks that [testApp2] layers fill the entire screen (i.e. is "fullscreen") at the end of the
      * transition once we have fully quick switched from [testApp1] back to the [testApp2].
      */
+    @FlakyTest(bugId = 298544839)
     @Test
     open fun endsWithApp2LayersCoveringFullScreen() {
         flicker.assertLayersEnd {
@@ -141,6 +143,7 @@
      * Checks that [testApp2] is the top window at the end of the transition once we have fully
      * quick switched from [testApp1] back to the [testApp2].
      */
+    @FlakyTest(bugId = 298544839)
     @Test
     open fun endsWithApp2BeingOnTop() {
         flicker.assertWmEnd { this.isAppWindowOnTop(testApp2) }
@@ -165,6 +168,7 @@
      * Checks that [testApp2]'s layer starts off invisible and becomes visible at some point before
      * the end of the transition and then stays visible until the end of the transition.
      */
+    @FlakyTest(bugId = 298544839)
     @Test
     open fun app2LayerBecomesAndStaysVisible() {
         flicker.assertLayers { this.isInvisible(testApp2).then().isVisible(testApp2) }
@@ -174,6 +178,7 @@
      * Checks that [testApp1]'s window starts off visible and becomes invisible at some point before
      * the end of the transition and then stays invisible until the end of the transition.
      */
+    @FlakyTest(bugId = 298544839)
     @Test
     open fun app1WindowBecomesAndStaysInvisible() {
         flicker.assertWm { this.isAppWindowVisible(testApp1).then().isAppWindowInvisible(testApp1) }
@@ -193,6 +198,7 @@
      * Ensures that at any point, either [testApp2] or [testApp1]'s windows are at least partially
      * visible.
      */
+    @FlakyTest(bugId = 298544839)
     @Test
     open fun app2WindowIsVisibleOnceApp1WindowIsInvisible() {
         flicker.assertWm {
@@ -211,6 +217,7 @@
      * Ensures that at any point, either [testApp2] or [testApp1]'s windows are at least partially
      * visible.
      */
+    @FlakyTest(bugId = 298544839)
     @Test
     open fun app2LayerIsVisibleOnceApp1LayerIsInvisible() {
         flicker.assertLayers {
@@ -225,6 +232,7 @@
     }
 
     /** {@inheritDoc} */
+    @FlakyTest(bugId = 298544839)
     @Test
     override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
 
@@ -233,31 +241,45 @@
     @Test
     override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
 
+    @FlakyTest(bugId = 298544839)
     @Test
     override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
         super.visibleLayersShownMoreThanOneConsecutiveEntry()
 
-    @Test override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
+    @FlakyTest(bugId = 298544839)
+    @Test
+    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
 
-    @Test override fun entireScreenCovered() = super.entireScreenCovered()
+    @FlakyTest(bugId = 298544839)
+    @Test
+    override fun entireScreenCovered() = super.entireScreenCovered()
 
+    @FlakyTest(bugId = 298544839)
     @Test
     override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
 
+    @FlakyTest(bugId = 298544839)
     @Test
     override fun navBarWindowIsVisibleAtStartAndEnd() = super.navBarWindowIsVisibleAtStartAndEnd()
 
+    @FlakyTest(bugId = 298544839)
     @Test
     override fun statusBarLayerIsVisibleAtStartAndEnd() =
         super.statusBarLayerIsVisibleAtStartAndEnd()
 
+    @FlakyTest(bugId = 298544839)
     @Test
     override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
 
-    @Test override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
+    @FlakyTest(bugId = 298544839)
+    @Test
+    override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
 
-    @Test override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
+    @FlakyTest(bugId = 298544839)
+    @Test
+    override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
 
+    @FlakyTest(bugId = 298544839)
     @Test
     override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
         super.visibleWindowsShownMoreThanOneConsecutiveEntry()
diff --git a/tests/permission/src/com/android/framework/permission/tests/WindowManagerPermissionTests.java b/tests/permission/src/com/android/framework/permission/tests/WindowManagerPermissionTests.java
index 06cbeb5..330bc84 100644
--- a/tests/permission/src/com/android/framework/permission/tests/WindowManagerPermissionTests.java
+++ b/tests/permission/src/com/android/framework/permission/tests/WindowManagerPermissionTests.java
@@ -124,7 +124,7 @@
     @SmallTest
     public void testSET_ORIENTATION() {
         try {
-            mWm.freezeRotation(-1);
+            mWm.freezeRotation(/* rotation= */ -1, /* caller= */ "WindowManagerPermissionTests");
             fail("IWindowManager.freezeRotation did not throw SecurityException as"
                     + " expected");
         } catch (SecurityException e) {
@@ -134,7 +134,7 @@
         }
 
         try {
-            mWm.thawRotation();
+            mWm.thawRotation(/* called= */ "WindowManagerPermissionTests");
             fail("IWindowManager.thawRotation did not throw SecurityException as"
                     + " expected");
         } catch (SecurityException e) {
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt
index 1aa4859..0c1d88a 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt
@@ -13,11 +13,11 @@
 }
 SourceFile: "HostSideTestClassLoadHook.java"
 RuntimeVisibleAnnotations:
-  0: #x(#x=[e#x.#x])
+  x: #x(#x=[e#x.#x])
     java.lang.annotation.Target(
       value=[Ljava/lang/annotation/ElementType;.TYPE]
     )
-  1: #x(#x=e#x.#x)
+  x: #x(#x=e#x.#x)
     java.lang.annotation.Retention(
       value=Ljava/lang/annotation/RetentionPolicy;.CLASS
     )
@@ -33,11 +33,11 @@
 }
 SourceFile: "HostSideTestKeep.java"
 RuntimeVisibleAnnotations:
-  0: #x(#x=[e#x.#x,e#x.#x,e#x.#x,e#x.#x])
+  x: #x(#x=[e#x.#x,e#x.#x,e#x.#x,e#x.#x])
     java.lang.annotation.Target(
       value=[Ljava/lang/annotation/ElementType;.TYPE,Ljava/lang/annotation/ElementType;.FIELD,Ljava/lang/annotation/ElementType;.METHOD,Ljava/lang/annotation/ElementType;.CONSTRUCTOR]
     )
-  1: #x(#x=e#x.#x)
+  x: #x(#x=e#x.#x)
     java.lang.annotation.Retention(
       value=Ljava/lang/annotation/RetentionPolicy;.CLASS
     )
@@ -56,11 +56,11 @@
 }
 SourceFile: "HostSideTestNativeSubstitutionClass.java"
 RuntimeVisibleAnnotations:
-  0: #x(#x=[e#x.#x])
+  x: #x(#x=[e#x.#x])
     java.lang.annotation.Target(
       value=[Ljava/lang/annotation/ElementType;.TYPE]
     )
-  1: #x(#x=e#x.#x)
+  x: #x(#x=e#x.#x)
     java.lang.annotation.Retention(
       value=Ljava/lang/annotation/RetentionPolicy;.CLASS
     )
@@ -76,11 +76,11 @@
 }
 SourceFile: "HostSideTestRemove.java"
 RuntimeVisibleAnnotations:
-  0: #x(#x=[e#x.#x,e#x.#x,e#x.#x,e#x.#x])
+  x: #x(#x=[e#x.#x,e#x.#x,e#x.#x,e#x.#x])
     java.lang.annotation.Target(
       value=[Ljava/lang/annotation/ElementType;.TYPE,Ljava/lang/annotation/ElementType;.FIELD,Ljava/lang/annotation/ElementType;.METHOD,Ljava/lang/annotation/ElementType;.CONSTRUCTOR]
     )
-  1: #x(#x=e#x.#x)
+  x: #x(#x=e#x.#x)
     java.lang.annotation.Retention(
       value=Ljava/lang/annotation/RetentionPolicy;.CLASS
     )
@@ -96,11 +96,11 @@
 }
 SourceFile: "HostSideTestStub.java"
 RuntimeVisibleAnnotations:
-  0: #x(#x=[e#x.#x,e#x.#x,e#x.#x,e#x.#x])
+  x: #x(#x=[e#x.#x,e#x.#x,e#x.#x,e#x.#x])
     java.lang.annotation.Target(
       value=[Ljava/lang/annotation/ElementType;.TYPE,Ljava/lang/annotation/ElementType;.FIELD,Ljava/lang/annotation/ElementType;.METHOD,Ljava/lang/annotation/ElementType;.CONSTRUCTOR]
     )
-  1: #x(#x=e#x.#x)
+  x: #x(#x=e#x.#x)
     java.lang.annotation.Retention(
       value=Ljava/lang/annotation/RetentionPolicy;.CLASS
     )
@@ -119,11 +119,11 @@
 }
 SourceFile: "HostSideTestSubstitute.java"
 RuntimeVisibleAnnotations:
-  0: #x(#x=[e#x.#x])
+  x: #x(#x=[e#x.#x])
     java.lang.annotation.Target(
       value=[Ljava/lang/annotation/ElementType;.METHOD]
     )
-  1: #x(#x=e#x.#x)
+  x: #x(#x=e#x.#x)
     java.lang.annotation.Retention(
       value=Ljava/lang/annotation/RetentionPolicy;.CLASS
     )
@@ -139,11 +139,11 @@
 }
 SourceFile: "HostSideTestThrow.java"
 RuntimeVisibleAnnotations:
-  0: #x(#x=[e#x.#x,e#x.#x])
+  x: #x(#x=[e#x.#x,e#x.#x])
     java.lang.annotation.Target(
       value=[Ljava/lang/annotation/ElementType;.METHOD,Ljava/lang/annotation/ElementType;.CONSTRUCTOR]
     )
-  1: #x(#x=e#x.#x)
+  x: #x(#x=e#x.#x)
     java.lang.annotation.Retention(
       value=Ljava/lang/annotation/RetentionPolicy;.CLASS
     )
@@ -159,11 +159,11 @@
 }
 SourceFile: "HostSideTestWholeClassKeep.java"
 RuntimeVisibleAnnotations:
-  0: #x(#x=[e#x.#x])
+  x: #x(#x=[e#x.#x])
     java.lang.annotation.Target(
       value=[Ljava/lang/annotation/ElementType;.TYPE]
     )
-  1: #x(#x=e#x.#x)
+  x: #x(#x=e#x.#x)
     java.lang.annotation.Retention(
       value=Ljava/lang/annotation/RetentionPolicy;.CLASS
     )
@@ -179,11 +179,11 @@
 }
 SourceFile: "HostSideTestWholeClassStub.java"
 RuntimeVisibleAnnotations:
-  0: #x(#x=[e#x.#x])
+  x: #x(#x=[e#x.#x])
     java.lang.annotation.Target(
       value=[Ljava/lang/annotation/ElementType;.TYPE]
     )
-  1: #x(#x=e#x.#x)
+  x: #x(#x=e#x.#x)
     java.lang.annotation.Retention(
       value=Ljava/lang/annotation/RetentionPolicy;.CLASS
     )
@@ -199,7 +199,7 @@
 }
 SourceFile: "HostSideTestSuppress.java"
 RuntimeVisibleAnnotations:
-  0: #x(#x=[e#x.#x,e#x.#x,e#x.#x])
+  x: #x(#x=[e#x.#x,e#x.#x,e#x.#x])
     java.lang.annotation.Target(
       value=[Ljava/lang/annotation/ElementType;.TYPE,Ljava/lang/annotation/ElementType;.FIELD,Ljava/lang/annotation/ElementType;.METHOD]
     )
@@ -217,9 +217,9 @@
     flags: (0x0002) ACC_PRIVATE
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                  // Method java/lang/Object."<init>":()V
-         4: return
+         x: aload_0
+         x: invokespecial #x                  // Method java/lang/Object."<init>":()V
+         x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -230,11 +230,11 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=1, locals=0, args_size=0
-         0: iconst_1
-         1: ireturn
+         x: iconst_1
+         x: ireturn
       LineNumberTable:
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestKeep
 
   public static int getOneStub();
@@ -242,11 +242,11 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=1, locals=0, args_size=0
-         0: iconst_1
-         1: ireturn
+         x: iconst_1
+         x: ireturn
       LineNumberTable:
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestStub
 }
 SourceFile: "TinyFrameworkCallerCheck.java"
@@ -267,9 +267,9 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                  // Method java/lang/Object."<init>":()V
-         4: return
+         x: aload_0
+         x: invokespecial #x                  // Method java/lang/Object."<init>":()V
+         x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -280,8 +280,8 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=1, locals=0, args_size=0
-         0: invokestatic  #x                  // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkCallerCheck$Impl.getOneKeep:()I
-         3: ireturn
+         x: invokestatic  #x                  // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkCallerCheck$Impl.getOneKeep:()I
+         x: ireturn
       LineNumberTable:
 
   public static int getOne_noCheck();
@@ -289,13 +289,13 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=1, locals=0, args_size=0
-         0: invokestatic  #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkCallerCheck$Impl.getOneStub:()I
-         3: ireturn
+         x: invokestatic  #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkCallerCheck$Impl.getOneStub:()I
+         x: ireturn
       LineNumberTable:
 }
 SourceFile: "TinyFrameworkCallerCheck.java"
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassStub
 NestMembers:
   com/android/hoststubgen/test/tinyframework/TinyFrameworkCallerCheck$Impl
@@ -314,14 +314,14 @@
     descriptor: I
     flags: (0x0001) ACC_PUBLIC
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestStub
 
   public int keep;
     descriptor: I
     flags: (0x0001) ACC_PUBLIC
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestKeep
 
   public int remove;
@@ -333,21 +333,21 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                  // Method java/lang/Object."<init>":()V
-         4: aload_0
-         5: iconst_1
-         6: putfield      #x                  // Field stub:I
-         9: aload_0
-        10: iconst_2
-        11: putfield      #x                 // Field keep:I
-        14: return
+         x: aload_0
+         x: invokespecial #x                  // Method java/lang/Object."<init>":()V
+         x: aload_0
+         x: iconst_1
+         x: putfield      #x                  // Field stub:I
+         x: aload_0
+        x: iconst_2
+        x: putfield      #x                 // Field keep:I
+        x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
             0      15     0  this   Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkClassAnnotations;
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestStub
 
   public int addOne(int);
@@ -355,17 +355,17 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=2, args_size=2
-         0: aload_0
-         1: iload_1
-         2: invokevirtual #x                 // Method addOneInner:(I)I
-         5: ireturn
+         x: aload_0
+         x: iload_1
+         x: invokevirtual #x                 // Method addOneInner:(I)I
+         x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
             0       6     0  this   Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkClassAnnotations;
             0       6     1 value   I
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestStub
 
   public int addOneInner(int);
@@ -373,17 +373,17 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=2, args_size=2
-         0: iload_1
-         1: iconst_1
-         2: iadd
-         3: ireturn
+         x: iload_1
+         x: iconst_1
+         x: iadd
+         x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
             0       4     0  this   Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkClassAnnotations;
             0       4     1 value   I
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestKeep
 
   public void toBeRemoved(java.lang.String);
@@ -391,17 +391,17 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=2, args_size=2
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: invokespecial #x                 // Method java/lang/RuntimeException."<init>":()V
-         7: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":()V
+         x: athrow
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
             0       8     0  this   Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkClassAnnotations;
             0       8     1   foo   Ljava/lang/String;
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestRemove
 
   public int addTwo(int);
@@ -409,20 +409,20 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=2, args_size=2
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String not supported on host side
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String not supported on host side
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
             0      10     0  this   Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkClassAnnotations;
             0      10     1 value   I
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestStub
-      1: #x(#x=s#x)
+      x: #x(#x=s#x)
         android.hosttest.annotation.HostSideTestSubstitute(
           suffix="_host"
         )
@@ -432,10 +432,10 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=2, args_size=2
-         0: iload_1
-         1: iconst_2
-         2: iadd
-         3: ireturn
+         x: iload_1
+         x: iconst_2
+         x: iadd
+         x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -446,9 +446,9 @@
     descriptor: (I)I
     flags: (0x0109) ACC_PUBLIC, ACC_STATIC, ACC_NATIVE
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestStub
-      1: #x(#x=s#x)
+      x: #x(#x=s#x)
         android.hosttest.annotation.HostSideTestSubstitute(
           suffix="_host"
         )
@@ -458,10 +458,10 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=2, locals=1, args_size=1
-         0: iload_0
-         1: iconst_3
-         2: iadd
-         3: ireturn
+         x: iload_0
+         x: iconst_3
+         x: iadd
+         x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -472,14 +472,14 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=1, locals=1, args_size=1
-         0: ldc           #x                 // String This value shouldn\'t be seen on the host side.
-         2: areturn
+         x: ldc           #x                 // String This value shouldn\'t be seen on the host side.
+         x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
             0       3     0  this   Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkClassAnnotations;
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestThrow
 
   public java.lang.String visibleButUsesUnsupportedMethod();
@@ -487,22 +487,22 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokevirtual #x                 // Method unsupportedMethod:()Ljava/lang/String;
-         4: areturn
+         x: aload_0
+         x: invokevirtual #x                 // Method unsupportedMethod:()Ljava/lang/String;
+         x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
             0       5     0  this   Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkClassAnnotations;
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestStub
 }
 SourceFile: "TinyFrameworkClassAnnotations.java"
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestStub
-  1: #x(#x=s#x)
+  x: #x(#x=s#x)
     android.hosttest.annotation.HostSideTestClassLoadHook(
       value="com.android.hoststubgen.test.tinyframework.TinyFrameworkClassLoadHook.onClassLoaded"
     )
@@ -532,15 +532,15 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                  // Method java/lang/Object."<init>":()V
-         4: aload_0
-         5: iconst_1
-         6: putfield      #x                  // Field stub:I
-         9: aload_0
-        10: iconst_2
-        11: putfield      #x                 // Field keep:I
-        14: return
+         x: aload_0
+         x: invokespecial #x                  // Method java/lang/Object."<init>":()V
+         x: aload_0
+         x: iconst_1
+         x: putfield      #x                  // Field stub:I
+         x: aload_0
+        x: iconst_2
+        x: putfield      #x                 // Field keep:I
+        x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -551,10 +551,10 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=2, args_size=2
-         0: aload_0
-         1: iload_1
-         2: invokevirtual #x                 // Method addOneInner:(I)I
-         5: ireturn
+         x: aload_0
+         x: iload_1
+         x: invokevirtual #x                 // Method addOneInner:(I)I
+         x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -566,10 +566,10 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=2, args_size=2
-         0: iload_1
-         1: iconst_1
-         2: iadd
-         3: ireturn
+         x: iload_1
+         x: iconst_1
+         x: iadd
+         x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -581,10 +581,10 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=2, args_size=2
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: invokespecial #x                 // Method java/lang/RuntimeException."<init>":()V
-         7: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":()V
+         x: athrow
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -596,20 +596,20 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=2, args_size=2
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String not supported on host side
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String not supported on host side
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
             0      10     0  this   Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkClassClassWideAnnotations;
             0      10     1 value   I
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestStub
-      1: #x(#x=s#x)
+      x: #x(#x=s#x)
         android.hosttest.annotation.HostSideTestSubstitute(
           suffix="_host"
         )
@@ -619,10 +619,10 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=2, args_size=2
-         0: iload_1
-         1: iconst_2
-         2: iadd
-         3: ireturn
+         x: iload_1
+         x: iconst_2
+         x: iadd
+         x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -633,9 +633,9 @@
     descriptor: (I)I
     flags: (0x0109) ACC_PUBLIC, ACC_STATIC, ACC_NATIVE
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestStub
-      1: #x(#x=s#x)
+      x: #x(#x=s#x)
         android.hosttest.annotation.HostSideTestSubstitute(
           suffix="_host"
         )
@@ -645,10 +645,10 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=2, locals=1, args_size=1
-         0: iload_0
-         1: iconst_3
-         2: iadd
-         3: ireturn
+         x: iload_0
+         x: iconst_3
+         x: iadd
+         x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -659,8 +659,8 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=1, locals=1, args_size=1
-         0: ldc           #x                 // String This value shouldn\'t be seen on the host side.
-         2: areturn
+         x: ldc           #x                 // String This value shouldn\'t be seen on the host side.
+         x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -671,9 +671,9 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokevirtual #x                 // Method unsupportedMethod:()Ljava/lang/String;
-         4: areturn
+         x: aload_0
+         x: invokevirtual #x                 // Method unsupportedMethod:()Ljava/lang/String;
+         x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -681,7 +681,7 @@
 }
 SourceFile: "TinyFrameworkClassClassWideAnnotations.java"
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassStub
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkClassLoadHook.class
   Compiled from "TinyFrameworkClassLoadHook.java"
@@ -702,9 +702,9 @@
     flags: (0x0002) ACC_PRIVATE
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                  // Method java/lang/Object."<init>":()V
-         4: return
+         x: aload_0
+         x: invokespecial #x                  // Method java/lang/Object."<init>":()V
+         x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -715,11 +715,11 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=2, locals=1, args_size=1
-         0: getstatic     #x                  // Field sLoadedClasses:Ljava/util/Set;
-         3: aload_0
-         4: invokeinterface #x,  2           // InterfaceMethod java/util/Set.add:(Ljava/lang/Object;)Z
-         9: pop
-        10: return
+         x: getstatic     #x                  // Field sLoadedClasses:Ljava/util/Set;
+         x: aload_0
+         x: invokeinterface #x,  2           // InterfaceMethod java/util/Set.add:(Ljava/lang/Object;)Z
+         x: pop
+        x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -734,16 +734,16 @@
     flags: (0x0008) ACC_STATIC
     Code:
       stack=2, locals=0, args_size=0
-         0: new           #x                 // class java/util/HashSet
-         3: dup
-         4: invokespecial #x                 // Method java/util/HashSet."<init>":()V
-         7: putstatic     #x                  // Field sLoadedClasses:Ljava/util/Set;
-        10: return
+         x: new           #x                 // class java/util/HashSet
+         x: dup
+         x: invokespecial #x                 // Method java/util/HashSet."<init>":()V
+         x: putstatic     #x                  // Field sLoadedClasses:Ljava/util/Set;
+        x: return
       LineNumberTable:
 }
 SourceFile: "TinyFrameworkClassLoadHook.java"
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassStub
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWithInitializer.class
   Compiled from "TinyFrameworkClassWithInitializer.java"
@@ -763,9 +763,9 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                  // Method java/lang/Object."<init>":()V
-         4: return
+         x: aload_0
+         x: invokespecial #x                  // Method java/lang/Object."<init>":()V
+         x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -776,18 +776,18 @@
     flags: (0x0008) ACC_STATIC
     Code:
       stack=1, locals=0, args_size=0
-         0: iconst_1
-         1: putstatic     #x                  // Field sInitialized:Z
-         4: return
+         x: iconst_1
+         x: putstatic     #x                  // Field sInitialized:Z
+         x: return
       LineNumberTable:
 }
 SourceFile: "TinyFrameworkClassWithInitializer.java"
 RuntimeInvisibleAnnotations:
-  0: #x(#x=s#x)
+  x: #x(#x=s#x)
     android.hosttest.annotation.HostSideTestClassLoadHook(
       value="com.android.hoststubgen.test.tinyframework.TinyFrameworkClassLoadHook.onClassLoaded"
     )
-  1: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassStub
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkExceptionTester.class
   Compiled from "TinyFrameworkExceptionTester.java"
@@ -803,9 +803,9 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                  // Method java/lang/Object."<init>":()V
-         4: return
+         x: aload_0
+         x: invokespecial #x                  // Method java/lang/Object."<init>":()V
+         x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -816,18 +816,18 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=4, locals=1, args_size=0
-         0: new           #x                  // class java/lang/IllegalStateException
-         3: dup
-         4: ldc           #x                  // String Inner exception
-         6: invokespecial #x                 // Method java/lang/IllegalStateException."<init>":(Ljava/lang/String;)V
-         9: athrow
-        10: astore_0
-        11: new           #x                 // class java/lang/RuntimeException
-        14: dup
-        15: ldc           #x                 // String Outer exception
-        17: aload_0
-        18: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;Ljava/lang/Throwable;)V
-        21: athrow
+         x: new           #x                  // class java/lang/IllegalStateException
+         x: dup
+         x: ldc           #x                  // String Inner exception
+         x: invokespecial #x                 // Method java/lang/IllegalStateException."<init>":(Ljava/lang/String;)V
+         x: athrow
+        x: astore_0
+        x: new           #x                 // class java/lang/RuntimeException
+        x: dup
+        x: ldc           #x                 // String Outer exception
+        x: aload_0
+        x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;Ljava/lang/Throwable;)V
+        x: athrow
       Exception table:
          from    to  target type
              0    10    10   Class java/lang/Exception
@@ -841,7 +841,7 @@
 }
 SourceFile: "TinyFrameworkExceptionTester.java"
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassStub
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkForTextPolicy.class
   Compiled from "TinyFrameworkForTextPolicy.java"
@@ -869,15 +869,15 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                  // Method java/lang/Object."<init>":()V
-         4: aload_0
-         5: iconst_1
-         6: putfield      #x                  // Field stub:I
-         9: aload_0
-        10: iconst_2
-        11: putfield      #x                 // Field keep:I
-        14: return
+         x: aload_0
+         x: invokespecial #x                  // Method java/lang/Object."<init>":()V
+         x: aload_0
+         x: iconst_1
+         x: putfield      #x                  // Field stub:I
+         x: aload_0
+        x: iconst_2
+        x: putfield      #x                 // Field keep:I
+        x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -888,10 +888,10 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=2, args_size=2
-         0: aload_0
-         1: iload_1
-         2: invokevirtual #x                 // Method addOneInner:(I)I
-         5: ireturn
+         x: aload_0
+         x: iload_1
+         x: invokevirtual #x                 // Method addOneInner:(I)I
+         x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -903,10 +903,10 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=2, args_size=2
-         0: iload_1
-         1: iconst_1
-         2: iadd
-         3: ireturn
+         x: iload_1
+         x: iconst_1
+         x: iadd
+         x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -918,10 +918,10 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=2, args_size=2
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: invokespecial #x                 // Method java/lang/RuntimeException."<init>":()V
-         7: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":()V
+         x: athrow
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -933,11 +933,11 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=2, args_size=2
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String not supported on host side
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String not supported on host side
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -949,10 +949,10 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=2, args_size=2
-         0: iload_1
-         1: iconst_2
-         2: iadd
-         3: ireturn
+         x: iload_1
+         x: iconst_2
+         x: iadd
+         x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -968,10 +968,10 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=2, locals=1, args_size=1
-         0: iload_0
-         1: iconst_3
-         2: iadd
-         3: ireturn
+         x: iload_0
+         x: iconst_3
+         x: iadd
+         x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -982,8 +982,8 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=1, locals=1, args_size=1
-         0: ldc           #x                 // String This value shouldn\'t be seen on the host side.
-         2: areturn
+         x: ldc           #x                 // String This value shouldn\'t be seen on the host side.
+         x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -994,9 +994,9 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokevirtual #x                 // Method unsupportedMethod:()Ljava/lang/String;
-         4: areturn
+         x: aload_0
+         x: invokevirtual #x                 // Method unsupportedMethod:()Ljava/lang/String;
+         x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1017,9 +1017,9 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                  // Method java/lang/Object."<init>":()V
-         4: return
+         x: aload_0
+         x: invokespecial #x                  // Method java/lang/Object."<init>":()V
+         x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1034,9 +1034,9 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=1, locals=1, args_size=1
-         0: iload_0
-         1: invokestatic  #x                  // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.nativeAddTwo:(I)I
-         4: ireturn
+         x: iload_0
+         x: invokestatic  #x                  // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.nativeAddTwo:(I)I
+         x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1051,10 +1051,10 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=4, locals=4, args_size=2
-         0: lload_0
-         1: lload_2
-         2: invokestatic  #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.nativeLongPlus:(JJ)J
-         5: lreturn
+         x: lload_0
+         x: lload_2
+         x: invokestatic  #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.nativeLongPlus:(JJ)J
+         x: lreturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1063,9 +1063,9 @@
 }
 SourceFile: "TinyFrameworkNative.java"
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassStub
-  1: #x(#x=s#x)
+  x: #x(#x=s#x)
     android.hosttest.annotation.HostSideTestNativeSubstitutionClass(
       value="TinyFrameworkNative_host"
     )
@@ -1083,9 +1083,9 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                  // Method java/lang/Object."<init>":()V
-         4: return
+         x: aload_0
+         x: invokespecial #x                  // Method java/lang/Object."<init>":()V
+         x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1096,10 +1096,10 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=2, locals=1, args_size=1
-         0: iload_0
-         1: iconst_2
-         2: iadd
-         3: ireturn
+         x: iload_0
+         x: iconst_2
+         x: iadd
+         x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1110,10 +1110,10 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=4, locals=4, args_size=2
-         0: lload_0
-         1: lload_2
-         2: ladd
-         3: lreturn
+         x: lload_0
+         x: lload_2
+         x: ladd
+         x: lreturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1122,7 +1122,7 @@
 }
 SourceFile: "TinyFrameworkNative_host.java"
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassKeep
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$1.class
   Compiled from "TinyFrameworkNestedClasses.java"
@@ -1142,12 +1142,12 @@
     flags: (0x0000)
     Code:
       stack=2, locals=2, args_size=2
-         0: aload_0
-         1: aload_1
-         2: putfield      #x                  // Field this$0:Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses;
-         5: aload_0
-         6: invokespecial #x                  // Method java/lang/Object."<init>":()V
-         9: return
+         x: aload_0
+         x: aload_1
+         x: putfield      #x                  // Field this$0:Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses;
+         x: aload_0
+         x: invokespecial #x                  // Method java/lang/Object."<init>":()V
+         x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1159,9 +1159,9 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=1, locals=1, args_size=1
-         0: iconst_1
-         1: invokestatic  #x                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
-         4: areturn
+         x: iconst_1
+         x: invokestatic  #x                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
+         x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1172,9 +1172,9 @@
     flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokevirtual #x                 // Method get:()Ljava/lang/Integer;
-         4: areturn
+         x: aload_0
+         x: invokevirtual #x                 // Method get:()Ljava/lang/Integer;
+         x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1200,9 +1200,9 @@
     flags: (0x0000)
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                  // Method java/lang/Object."<init>":()V
-         4: return
+         x: aload_0
+         x: invokespecial #x                  // Method java/lang/Object."<init>":()V
+         x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1213,9 +1213,9 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=1, locals=1, args_size=1
-         0: iconst_2
-         1: invokestatic  #x                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
-         4: areturn
+         x: iconst_2
+         x: invokestatic  #x                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
+         x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1226,9 +1226,9 @@
     flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokevirtual #x                 // Method get:()Ljava/lang/Integer;
-         4: areturn
+         x: aload_0
+         x: invokevirtual #x                 // Method get:()Ljava/lang/Integer;
+         x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1258,12 +1258,12 @@
     flags: (0x0000)
     Code:
       stack=2, locals=2, args_size=2
-         0: aload_0
-         1: aload_1
-         2: putfield      #x                  // Field this$0:Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses;
-         5: aload_0
-         6: invokespecial #x                  // Method java/lang/Object."<init>":()V
-         9: return
+         x: aload_0
+         x: aload_1
+         x: putfield      #x                  // Field this$0:Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses;
+         x: aload_0
+         x: invokespecial #x                  // Method java/lang/Object."<init>":()V
+         x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1275,9 +1275,9 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=1, locals=1, args_size=1
-         0: iconst_3
-         1: invokestatic  #x                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
-         4: areturn
+         x: iconst_3
+         x: invokestatic  #x                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
+         x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1288,9 +1288,9 @@
     flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokevirtual #x                 // Method get:()Ljava/lang/Integer;
-         4: areturn
+         x: aload_0
+         x: invokevirtual #x                 // Method get:()Ljava/lang/Integer;
+         x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1316,9 +1316,9 @@
     flags: (0x0000)
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                  // Method java/lang/Object."<init>":()V
-         4: return
+         x: aload_0
+         x: invokespecial #x                  // Method java/lang/Object."<init>":()V
+         x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1329,9 +1329,9 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=1, locals=1, args_size=1
-         0: iconst_4
-         1: invokestatic  #x                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
-         4: areturn
+         x: iconst_4
+         x: invokestatic  #x                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
+         x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1342,9 +1342,9 @@
     flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokevirtual #x                 // Method get:()Ljava/lang/Integer;
-         4: areturn
+         x: aload_0
+         x: invokevirtual #x                 // Method get:()Ljava/lang/Integer;
+         x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1374,12 +1374,12 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=2, args_size=2
-         0: aload_0
-         1: invokespecial #x                  // Method java/lang/Object."<init>":()V
-         4: aload_0
-         5: iload_1
-         6: putfield      #x                  // Field value:I
-         9: return
+         x: aload_0
+         x: invokespecial #x                  // Method java/lang/Object."<init>":()V
+         x: aload_0
+         x: iload_1
+         x: putfield      #x                  // Field value:I
+         x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1412,15 +1412,15 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=2, args_size=2
-         0: aload_0
-         1: aload_1
-         2: putfield      #x                  // Field this$0:Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses;
-         5: aload_0
-         6: invokespecial #x                  // Method java/lang/Object."<init>":()V
-         9: aload_0
-        10: iconst_5
-        11: putfield      #x                 // Field value:I
-        14: return
+         x: aload_0
+         x: aload_1
+         x: putfield      #x                  // Field this$0:Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses;
+         x: aload_0
+         x: invokespecial #x                  // Method java/lang/Object."<init>":()V
+         x: aload_0
+        x: iconst_5
+        x: putfield      #x                 // Field value:I
+        x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1429,7 +1429,7 @@
 }
 SourceFile: "TinyFrameworkNestedClasses.java"
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassStub
 NestHost: class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses
 InnerClasses:
@@ -1448,9 +1448,9 @@
     flags: (0x0000)
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                  // Method java/lang/Object."<init>":()V
-         4: return
+         x: aload_0
+         x: invokespecial #x                  // Method java/lang/Object."<init>":()V
+         x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1461,9 +1461,9 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=1, locals=1, args_size=1
-         0: bipush        7
-         2: invokestatic  #x                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
-         5: areturn
+         x: bipush        7
+         x: invokestatic  #x                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
+         x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1474,9 +1474,9 @@
     flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokevirtual #x                 // Method get:()Ljava/lang/Integer;
-         4: areturn
+         x: aload_0
+         x: invokevirtual #x                 // Method get:()Ljava/lang/Integer;
+         x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1507,12 +1507,12 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                  // Method java/lang/Object."<init>":()V
-         4: aload_0
-         5: bipush        6
-         7: putfield      #x                  // Field value:I
-        10: return
+         x: aload_0
+         x: invokespecial #x                  // Method java/lang/Object."<init>":()V
+         x: aload_0
+         x: bipush        6
+         x: putfield      #x                  // Field value:I
+        x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1523,16 +1523,16 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=2, locals=0, args_size=0
-         0: new           #x                 // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$StaticNestedClass$1
-         3: dup
-         4: invokespecial #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$StaticNestedClass$1."<init>":()V
-         7: areturn
+         x: new           #x                 // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$StaticNestedClass$1
+         x: dup
+         x: invokespecial #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$StaticNestedClass$1."<init>":()V
+         x: areturn
       LineNumberTable:
     Signature: #x                          // ()Ljava/util/function/Supplier<Ljava/lang/Integer;>;
 }
 SourceFile: "TinyFrameworkNestedClasses.java"
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassStub
 NestHost: class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses
 InnerClasses:
@@ -1552,10 +1552,10 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=2, args_size=2
-         0: aload_0
-         1: iload_1
-         2: invokespecial #x                  // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$BaseClass."<init>":(I)V
-         5: return
+         x: aload_0
+         x: iload_1
+         x: invokespecial #x                  // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$BaseClass."<init>":(I)V
+         x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1591,15 +1591,15 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=4, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                  // Method java/lang/Object."<init>":()V
-         4: aload_0
-         5: new           #x                  // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$1
-         8: dup
-         9: aload_0
-        10: invokespecial #x                  // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$1."<init>":(Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses;)V
-        13: putfield      #x                 // Field mSupplier:Ljava/util/function/Supplier;
-        16: return
+         x: aload_0
+         x: invokespecial #x                  // Method java/lang/Object."<init>":()V
+         x: aload_0
+         x: new           #x                  // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$1
+         x: dup
+         x: aload_0
+        x: invokespecial #x                  // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$1."<init>":(Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses;)V
+        x: putfield      #x                 // Field mSupplier:Ljava/util/function/Supplier;
+        x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1610,11 +1610,11 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=1, args_size=1
-         0: new           #x                 // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$3
-         3: dup
-         4: aload_0
-         5: invokespecial #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$3."<init>":(Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses;)V
-         8: areturn
+         x: new           #x                 // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$3
+         x: dup
+         x: aload_0
+         x: invokespecial #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$3."<init>":(Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses;)V
+         x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1626,10 +1626,10 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=2, locals=0, args_size=0
-         0: new           #x                 // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$4
-         3: dup
-         4: invokespecial #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$4."<init>":()V
-         7: areturn
+         x: new           #x                 // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$4
+         x: dup
+         x: invokespecial #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$4."<init>":()V
+         x: areturn
       LineNumberTable:
     Signature: #x                          // ()Ljava/util/function/Supplier<Ljava/lang/Integer;>;
 
@@ -1638,16 +1638,16 @@
     flags: (0x0008) ACC_STATIC
     Code:
       stack=2, locals=0, args_size=0
-         0: new           #x                 // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$2
-         3: dup
-         4: invokespecial #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$2."<init>":()V
-         7: putstatic     #x                 // Field sSupplier:Ljava/util/function/Supplier;
-        10: return
+         x: new           #x                 // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$2
+         x: dup
+         x: invokespecial #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$2."<init>":()V
+         x: putstatic     #x                 // Field sSupplier:Ljava/util/function/Supplier;
+        x: return
       LineNumberTable:
 }
 SourceFile: "TinyFrameworkNestedClasses.java"
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassStub
 NestMembers:
   com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$SubClass
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/02-hoststubgen-test-tiny-framework-host-stub-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/02-hoststubgen-test-tiny-framework-host-stub-dump.txt
index 6e1528a..43ceec4 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/02-hoststubgen-test-tiny-framework-host-stub-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/02-hoststubgen-test-tiny-framework-host-stub-dump.txt
@@ -12,33 +12,33 @@
     flags: (0x0002) ACC_PRIVATE
     Code:
       stack=3, locals=1, args_size=1
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 
   public static int getOneStub();
     descriptor: ()I
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=3, locals=0, args_size=0
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestStub
 }
 InnerClasses:
   private static #x= #x of #x;           // Impl=class com/android/hoststubgen/test/tinyframework/TinyFrameworkCallerCheck$Impl of class com/android/hoststubgen/test/tinyframework/TinyFrameworkCallerCheck
 SourceFile: "TinyFrameworkCallerCheck.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 NestHost: class com/android/hoststubgen/test/tinyframework/TinyFrameworkCallerCheck
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkCallerCheck.class
@@ -55,44 +55,44 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=1, args_size=1
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 
   public static int getOne_withCheck();
     descriptor: ()I
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=3, locals=0, args_size=0
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 
   public static int getOne_noCheck();
     descriptor: ()I
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=3, locals=0, args_size=0
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 }
 InnerClasses:
   private static #x= #x of #x;          // Impl=class com/android/hoststubgen/test/tinyframework/TinyFrameworkCallerCheck$Impl of class com/android/hoststubgen/test/tinyframework/TinyFrameworkCallerCheck
 SourceFile: "TinyFrameworkCallerCheck.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassStub
 NestMembers:
   com/android/hoststubgen/test/tinyframework/TinyFrameworkCallerCheck$Impl
@@ -109,7 +109,7 @@
     descriptor: I
     flags: (0x0001) ACC_PUBLIC
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestStub
 
   public com.android.hoststubgen.test.tinyframework.TinyFrameworkClassAnnotations();
@@ -117,13 +117,13 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=1, args_size=1
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestStub
 
   public int addOne(int);
@@ -131,13 +131,13 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=2, args_size=2
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestStub
 
   public int addTwo(int);
@@ -145,47 +145,47 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=2, args_size=2
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 
   public static int nativeAddThree(int);
     descriptor: (I)I
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=3, locals=1, args_size=1
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 
   public java.lang.String visibleButUsesUnsupportedMethod();
     descriptor: ()Ljava/lang/String;
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=1, args_size=1
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestStub
 }
 SourceFile: "TinyFrameworkClassAnnotations.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestStub
-  1: #x(#x=s#x)
+  x: #x(#x=s#x)
     android.hosttest.annotation.HostSideTestClassLoadHook(
       value="com.android.hoststubgen.test.tinyframework.TinyFrameworkClassLoadHook.onClassLoaded"
     )
@@ -215,97 +215,97 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=1, args_size=1
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 
   public int addOne(int);
     descriptor: (I)I
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=2, args_size=2
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 
   public int addOneInner(int);
     descriptor: (I)I
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=2, args_size=2
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 
   public void toBeRemoved(java.lang.String);
     descriptor: (Ljava/lang/String;)V
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=2, args_size=2
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 
   public int addTwo(int);
     descriptor: (I)I
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=2, args_size=2
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 
   public static int nativeAddThree(int);
     descriptor: (I)I
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=3, locals=1, args_size=1
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 
   public java.lang.String unsupportedMethod();
     descriptor: ()Ljava/lang/String;
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=1, args_size=1
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 
   public java.lang.String visibleButUsesUnsupportedMethod();
     descriptor: ()Ljava/lang/String;
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=1, args_size=1
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 }
 SourceFile: "TinyFrameworkClassClassWideAnnotations.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassStub
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkClassLoadHook.class
   Compiled from "TinyFrameworkClassLoadHook.java"
@@ -326,22 +326,22 @@
     flags: (0x0002) ACC_PRIVATE
     Code:
       stack=3, locals=1, args_size=1
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 
   public static void onClassLoaded(java.lang.Class<?>);
     descriptor: (Ljava/lang/Class;)V
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=3, locals=1, args_size=1
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
     Signature: #x                          // (Ljava/lang/Class<*>;)V
 
   static {};
@@ -349,20 +349,20 @@
     flags: (0x0008) ACC_STATIC
     Code:
       stack=3, locals=0, args_size=0
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 }
 SourceFile: "TinyFrameworkClassLoadHook.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassStub
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWithInitializer.class
   Compiled from "TinyFrameworkClassWithInitializer.java"
@@ -382,35 +382,35 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=1, args_size=1
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 
   static {};
     descriptor: ()V
     flags: (0x0008) ACC_STATIC
     Code:
       stack=3, locals=0, args_size=0
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 }
 SourceFile: "TinyFrameworkClassWithInitializer.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 RuntimeInvisibleAnnotations:
-  0: #x(#x=s#x)
+  x: #x(#x=s#x)
     android.hosttest.annotation.HostSideTestClassLoadHook(
       value="com.android.hoststubgen.test.tinyframework.TinyFrameworkClassLoadHook.onClassLoaded"
     )
-  1: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassStub
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkExceptionTester.class
   Compiled from "TinyFrameworkExceptionTester.java"
@@ -426,31 +426,31 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=1, args_size=1
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 
   public static int testException();
     descriptor: ()I
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=3, locals=0, args_size=0
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 }
 SourceFile: "TinyFrameworkExceptionTester.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassStub
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkForTextPolicy.class
   Compiled from "TinyFrameworkForTextPolicy.java"
@@ -470,61 +470,61 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=1, args_size=1
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 
   public int addOne(int);
     descriptor: (I)I
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=2, args_size=2
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 
   public int addTwo(int);
     descriptor: (I)I
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=2, args_size=2
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 
   public static int nativeAddThree(int);
     descriptor: (I)I
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=3, locals=1, args_size=1
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 
   public java.lang.String visibleButUsesUnsupportedMethod();
     descriptor: ()Ljava/lang/String;
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=1, args_size=1
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 }
 SourceFile: "TinyFrameworkForTextPolicy.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkNative.class
   Compiled from "TinyFrameworkNative.java"
@@ -540,11 +540,11 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=1, args_size=1
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 
   public static native int nativeAddTwo(int);
     descriptor: (I)I
@@ -555,11 +555,11 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=3, locals=1, args_size=1
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 
   public static native long nativeLongPlus(long, long);
     descriptor: (JJ)J
@@ -570,22 +570,22 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=3, locals=4, args_size=2
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 }
 SourceFile: "TinyFrameworkNative.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassStub
-  1: #x(#x=s#x)
+  x: #x(#x=s#x)
     android.hosttest.annotation.HostSideTestNativeSubstitutionClass(
       value="TinyFrameworkNative_host"
     )
@@ -607,19 +607,19 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=2, args_size=2
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 }
 InnerClasses:
   public static #x= #x of #x;            // BaseClass=class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$BaseClass of class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses
 SourceFile: "TinyFrameworkNestedClasses.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 NestHost: class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$InnerClass.class
@@ -644,22 +644,22 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=2, args_size=2
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 }
 InnerClasses:
   public #x= #x of #x;                   // InnerClass=class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$InnerClass of class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses
 SourceFile: "TinyFrameworkNestedClasses.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassStub
 NestHost: class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$StaticNestedClass.class
@@ -680,22 +680,22 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=1, args_size=1
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 
   public static java.util.function.Supplier<java.lang.Integer> getSupplier_static();
     descriptor: ()Ljava/util/function/Supplier;
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=3, locals=0, args_size=0
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
     Signature: #x                          // ()Ljava/util/function/Supplier<Ljava/lang/Integer;>;
 }
 InnerClasses:
@@ -703,12 +703,12 @@
   #x;                                    // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$StaticNestedClass$1
 SourceFile: "TinyFrameworkNestedClasses.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassStub
 NestHost: class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$SubClass.class
@@ -725,20 +725,20 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=2, args_size=2
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 }
 InnerClasses:
   public static #x= #x of #x;            // BaseClass=class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$BaseClass of class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses
   public static #x= #x of #x;            // SubClass=class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$SubClass of class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses
 SourceFile: "TinyFrameworkNestedClasses.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 NestHost: class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses.class
@@ -765,22 +765,22 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=1, args_size=1
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 
   public java.util.function.Supplier<java.lang.Integer> getSupplier();
     descriptor: ()Ljava/util/function/Supplier;
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=1, args_size=1
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
     Signature: #x                          // ()Ljava/util/function/Supplier<Ljava/lang/Integer;>;
 
   public static java.util.function.Supplier<java.lang.Integer> getSupplier_static();
@@ -788,11 +788,11 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=3, locals=0, args_size=0
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
     Signature: #x                          // ()Ljava/util/function/Supplier<Ljava/lang/Integer;>;
 
   static {};
@@ -800,11 +800,11 @@
     flags: (0x0008) ACC_STATIC
     Code:
       stack=3, locals=0, args_size=0
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: ldc           #x                 // String Stub!
-         6: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-         9: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: ldc           #x                 // String Stub!
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+         x: athrow
 }
 InnerClasses:
   #x;                                    // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$1
@@ -818,12 +818,12 @@
   #x;                                    // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$StaticNestedClass$1
 SourceFile: "TinyFrameworkNestedClasses.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassStub
 NestMembers:
   com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$SubClass
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-impl-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-impl-dump.txt
index 5672e9c..faf0a46 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-impl-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-impl-dump.txt
@@ -13,13 +13,13 @@
 }
 SourceFile: "HostSideTestClassLoadHook.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
-  1: #x(#x=[e#x.#x])
+  x: #x(#x=[e#x.#x])
     java.lang.annotation.Target(
       value=[Ljava/lang/annotation/ElementType;.TYPE]
     )
-  2: #x(#x=e#x.#x)
+  x: #x(#x=e#x.#x)
     java.lang.annotation.Retention(
       value=Ljava/lang/annotation/RetentionPolicy;.CLASS
     )
@@ -35,13 +35,13 @@
 }
 SourceFile: "HostSideTestKeep.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
-  1: #x(#x=[e#x.#x,e#x.#x,e#x.#x,e#x.#x])
+  x: #x(#x=[e#x.#x,e#x.#x,e#x.#x,e#x.#x])
     java.lang.annotation.Target(
       value=[Ljava/lang/annotation/ElementType;.TYPE,Ljava/lang/annotation/ElementType;.FIELD,Ljava/lang/annotation/ElementType;.METHOD,Ljava/lang/annotation/ElementType;.CONSTRUCTOR]
     )
-  2: #x(#x=e#x.#x)
+  x: #x(#x=e#x.#x)
     java.lang.annotation.Retention(
       value=Ljava/lang/annotation/RetentionPolicy;.CLASS
     )
@@ -60,13 +60,13 @@
 }
 SourceFile: "HostSideTestNativeSubstitutionClass.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
-  1: #x(#x=[e#x.#x])
+  x: #x(#x=[e#x.#x])
     java.lang.annotation.Target(
       value=[Ljava/lang/annotation/ElementType;.TYPE]
     )
-  2: #x(#x=e#x.#x)
+  x: #x(#x=e#x.#x)
     java.lang.annotation.Retention(
       value=Ljava/lang/annotation/RetentionPolicy;.CLASS
     )
@@ -82,13 +82,13 @@
 }
 SourceFile: "HostSideTestRemove.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
-  1: #x(#x=[e#x.#x,e#x.#x,e#x.#x,e#x.#x])
+  x: #x(#x=[e#x.#x,e#x.#x,e#x.#x,e#x.#x])
     java.lang.annotation.Target(
       value=[Ljava/lang/annotation/ElementType;.TYPE,Ljava/lang/annotation/ElementType;.FIELD,Ljava/lang/annotation/ElementType;.METHOD,Ljava/lang/annotation/ElementType;.CONSTRUCTOR]
     )
-  2: #x(#x=e#x.#x)
+  x: #x(#x=e#x.#x)
     java.lang.annotation.Retention(
       value=Ljava/lang/annotation/RetentionPolicy;.CLASS
     )
@@ -104,13 +104,13 @@
 }
 SourceFile: "HostSideTestStub.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
-  1: #x(#x=[e#x.#x,e#x.#x,e#x.#x,e#x.#x])
+  x: #x(#x=[e#x.#x,e#x.#x,e#x.#x,e#x.#x])
     java.lang.annotation.Target(
       value=[Ljava/lang/annotation/ElementType;.TYPE,Ljava/lang/annotation/ElementType;.FIELD,Ljava/lang/annotation/ElementType;.METHOD,Ljava/lang/annotation/ElementType;.CONSTRUCTOR]
     )
-  2: #x(#x=e#x.#x)
+  x: #x(#x=e#x.#x)
     java.lang.annotation.Retention(
       value=Ljava/lang/annotation/RetentionPolicy;.CLASS
     )
@@ -129,13 +129,13 @@
 }
 SourceFile: "HostSideTestSubstitute.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
-  1: #x(#x=[e#x.#x])
+  x: #x(#x=[e#x.#x])
     java.lang.annotation.Target(
       value=[Ljava/lang/annotation/ElementType;.METHOD]
     )
-  2: #x(#x=e#x.#x)
+  x: #x(#x=e#x.#x)
     java.lang.annotation.Retention(
       value=Ljava/lang/annotation/RetentionPolicy;.CLASS
     )
@@ -151,13 +151,13 @@
 }
 SourceFile: "HostSideTestThrow.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
-  1: #x(#x=[e#x.#x,e#x.#x])
+  x: #x(#x=[e#x.#x,e#x.#x])
     java.lang.annotation.Target(
       value=[Ljava/lang/annotation/ElementType;.METHOD,Ljava/lang/annotation/ElementType;.CONSTRUCTOR]
     )
-  2: #x(#x=e#x.#x)
+  x: #x(#x=e#x.#x)
     java.lang.annotation.Retention(
       value=Ljava/lang/annotation/RetentionPolicy;.CLASS
     )
@@ -173,13 +173,13 @@
 }
 SourceFile: "HostSideTestWholeClassKeep.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
-  1: #x(#x=[e#x.#x])
+  x: #x(#x=[e#x.#x])
     java.lang.annotation.Target(
       value=[Ljava/lang/annotation/ElementType;.TYPE]
     )
-  2: #x(#x=e#x.#x)
+  x: #x(#x=e#x.#x)
     java.lang.annotation.Retention(
       value=Ljava/lang/annotation/RetentionPolicy;.CLASS
     )
@@ -195,13 +195,13 @@
 }
 SourceFile: "HostSideTestWholeClassStub.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
-  1: #x(#x=[e#x.#x])
+  x: #x(#x=[e#x.#x])
     java.lang.annotation.Target(
       value=[Ljava/lang/annotation/ElementType;.TYPE]
     )
-  2: #x(#x=e#x.#x)
+  x: #x(#x=e#x.#x)
     java.lang.annotation.Retention(
       value=Ljava/lang/annotation/RetentionPolicy;.CLASS
     )
@@ -219,9 +219,9 @@
     flags: (0x0002) ACC_PRIVATE
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                 // Method java/lang/Object."<init>":()V
-         4: return
+         x: aload_0
+         x: invokespecial #x                 // Method java/lang/Object."<init>":()V
+         x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -232,17 +232,17 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=4, locals=0, args_size=0
-         0: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkCallerCheck$Impl
-         2: ldc           #x                 // String getOneKeep
-         4: ldc           #x                 // String ()I
-         6: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
-         9: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
-        12: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
-        15: iconst_1
-        16: ireturn
+         x: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkCallerCheck$Impl
+         x: ldc           #x                 // String getOneKeep
+         x: ldc           #x                 // String ()I
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
+         x: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
+        x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
+        x: iconst_1
+        x: ireturn
       LineNumberTable:
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestKeep
 
   public static int getOneStub();
@@ -250,20 +250,20 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=1, locals=0, args_size=0
-         0: iconst_1
-         1: ireturn
+         x: iconst_1
+         x: ireturn
       LineNumberTable:
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestStub
 }
 InnerClasses:
   private static #x= #x of #x;           // Impl=class com/android/hoststubgen/test/tinyframework/TinyFrameworkCallerCheck$Impl of class com/android/hoststubgen/test/tinyframework/TinyFrameworkCallerCheck
 SourceFile: "TinyFrameworkCallerCheck.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 NestHost: class com/android/hoststubgen/test/tinyframework/TinyFrameworkCallerCheck
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkCallerCheck.class
@@ -280,9 +280,9 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                 // Method java/lang/Object."<init>":()V
-         4: return
+         x: aload_0
+         x: invokespecial #x                 // Method java/lang/Object."<init>":()V
+         x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -293,8 +293,8 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=1, locals=0, args_size=0
-         0: invokestatic  #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkCallerCheck$Impl.getOneKeep:()I
-         3: ireturn
+         x: invokestatic  #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkCallerCheck$Impl.getOneKeep:()I
+         x: ireturn
       LineNumberTable:
 
   public static int getOne_noCheck();
@@ -302,20 +302,20 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=1, locals=0, args_size=0
-         0: invokestatic  #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkCallerCheck$Impl.getOneStub:()I
-         3: ireturn
+         x: invokestatic  #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkCallerCheck$Impl.getOneStub:()I
+         x: ireturn
       LineNumberTable:
 }
 InnerClasses:
   private static #x= #x of #x;          // Impl=class com/android/hoststubgen/test/tinyframework/TinyFrameworkCallerCheck$Impl of class com/android/hoststubgen/test/tinyframework/TinyFrameworkCallerCheck
 SourceFile: "TinyFrameworkCallerCheck.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassStub
 NestMembers:
   com/android/hoststubgen/test/tinyframework/TinyFrameworkCallerCheck$Impl
@@ -332,14 +332,14 @@
     descriptor: I
     flags: (0x0001) ACC_PUBLIC
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestStub
 
   public int keep;
     descriptor: I
     flags: (0x0001) ACC_PUBLIC
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestKeep
 
   private static {};
@@ -347,31 +347,31 @@
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
     Code:
       stack=2, locals=0, args_size=0
-         0: ldc           #x                  // class com/android/hoststubgen/test/tinyframework/TinyFrameworkClassAnnotations
-         2: ldc           #x                 // String com.android.hoststubgen.test.tinyframework.TinyFrameworkClassLoadHook.onClassLoaded
-         4: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
-         7: return
+         x: ldc           #x                  // class com/android/hoststubgen/test/tinyframework/TinyFrameworkClassAnnotations
+         x: ldc           #x                 // String com.android.hoststubgen.test.tinyframework.TinyFrameworkClassLoadHook.onClassLoaded
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
+         x: return
 
   public com.android.hoststubgen.test.tinyframework.TinyFrameworkClassAnnotations();
     descriptor: ()V
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                 // Method java/lang/Object."<init>":()V
-         4: aload_0
-         5: iconst_1
-         6: putfield      #x                 // Field stub:I
-         9: aload_0
-        10: iconst_2
-        11: putfield      #x                 // Field keep:I
-        14: return
+         x: aload_0
+         x: invokespecial #x                 // Method java/lang/Object."<init>":()V
+         x: aload_0
+         x: iconst_1
+         x: putfield      #x                 // Field stub:I
+         x: aload_0
+        x: iconst_2
+        x: putfield      #x                 // Field keep:I
+        x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
             0      15     0  this   Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkClassAnnotations;
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestStub
 
   public int addOne(int);
@@ -379,17 +379,17 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=2, args_size=2
-         0: aload_0
-         1: iload_1
-         2: invokevirtual #x                 // Method addOneInner:(I)I
-         5: ireturn
+         x: aload_0
+         x: iload_1
+         x: invokevirtual #x                 // Method addOneInner:(I)I
+         x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
             0       6     0  this   Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkClassAnnotations;
             0       6     1 value   I
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestStub
 
   public int addOneInner(int);
@@ -397,23 +397,23 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=4, locals=2, args_size=2
-         0: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkClassAnnotations
-         2: ldc           #x                 // String addOneInner
-         4: ldc           #x                 // String (I)I
-         6: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
-         9: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
-        12: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
-        15: iload_1
-        16: iconst_1
-        17: iadd
-        18: ireturn
+         x: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkClassAnnotations
+         x: ldc           #x                 // String addOneInner
+         x: ldc           #x                 // String (I)I
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
+         x: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
+        x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
+        x: iload_1
+        x: iconst_1
+        x: iadd
+        x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
            15       4     0  this   Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkClassAnnotations;
            15       4     1 value   I
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestKeep
 
   public int addTwo(int);
@@ -421,10 +421,10 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=2, args_size=2
-         0: iload_1
-         1: iconst_2
-         2: iadd
-         3: ireturn
+         x: iload_1
+         x: iconst_2
+         x: iadd
+         x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -436,10 +436,10 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=2, locals=1, args_size=1
-         0: iload_0
-         1: iconst_3
-         2: iadd
-         3: ireturn
+         x: iload_0
+         x: iconst_3
+         x: iadd
+         x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -450,20 +450,20 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=4, locals=1, args_size=1
-         0: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkClassAnnotations
-         2: ldc           #x                 // String unsupportedMethod
-         4: ldc           #x                 // String ()Ljava/lang/String;
-         6: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
-         9: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
-        12: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
-        15: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onThrowMethodCalled:()V
-        18: new           #x                 // class java/lang/RuntimeException
-        21: dup
-        22: ldc           #x                 // String Unreachable
-        24: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-        27: athrow
+         x: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkClassAnnotations
+         x: ldc           #x                 // String unsupportedMethod
+         x: ldc           #x                 // String ()Ljava/lang/String;
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
+         x: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
+        x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
+        x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onThrowMethodCalled:()V
+        x: new           #x                 // class java/lang/RuntimeException
+        x: dup
+        x: ldc           #x                 // String Unreachable
+        x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+        x: athrow
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestThrow
 
   public java.lang.String visibleButUsesUnsupportedMethod();
@@ -471,27 +471,27 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokevirtual #x                 // Method unsupportedMethod:()Ljava/lang/String;
-         4: areturn
+         x: aload_0
+         x: invokevirtual #x                 // Method unsupportedMethod:()Ljava/lang/String;
+         x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
             0       5     0  this   Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkClassAnnotations;
     RuntimeInvisibleAnnotations:
-      0: #x()
+      x: #x()
         android.hosttest.annotation.HostSideTestStub
 }
 SourceFile: "TinyFrameworkClassAnnotations.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestStub
-  1: #x(#x=s#x)
+  x: #x(#x=s#x)
     android.hosttest.annotation.HostSideTestClassLoadHook(
       value="com.android.hoststubgen.test.tinyframework.TinyFrameworkClassLoadHook.onClassLoaded"
     )
@@ -521,15 +521,15 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                 // Method java/lang/Object."<init>":()V
-         4: aload_0
-         5: iconst_1
-         6: putfield      #x                 // Field stub:I
-         9: aload_0
-        10: iconst_2
-        11: putfield      #x                 // Field keep:I
-        14: return
+         x: aload_0
+         x: invokespecial #x                 // Method java/lang/Object."<init>":()V
+         x: aload_0
+         x: iconst_1
+         x: putfield      #x                 // Field stub:I
+         x: aload_0
+        x: iconst_2
+        x: putfield      #x                 // Field keep:I
+        x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -540,10 +540,10 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=2, args_size=2
-         0: aload_0
-         1: iload_1
-         2: invokevirtual #x                 // Method addOneInner:(I)I
-         5: ireturn
+         x: aload_0
+         x: iload_1
+         x: invokevirtual #x                 // Method addOneInner:(I)I
+         x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -555,10 +555,10 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=2, args_size=2
-         0: iload_1
-         1: iconst_1
-         2: iadd
-         3: ireturn
+         x: iload_1
+         x: iconst_1
+         x: iadd
+         x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -570,10 +570,10 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=2, args_size=2
-         0: new           #x                 // class java/lang/RuntimeException
-         3: dup
-         4: invokespecial #x                 // Method java/lang/RuntimeException."<init>":()V
-         7: athrow
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":()V
+         x: athrow
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -585,10 +585,10 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=2, args_size=2
-         0: iload_1
-         1: iconst_2
-         2: iadd
-         3: ireturn
+         x: iload_1
+         x: iconst_2
+         x: iadd
+         x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -600,10 +600,10 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=2, locals=1, args_size=1
-         0: iload_0
-         1: iconst_3
-         2: iadd
-         3: ireturn
+         x: iload_0
+         x: iconst_3
+         x: iadd
+         x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -614,8 +614,8 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=1, locals=1, args_size=1
-         0: ldc           #x                 // String This value shouldn\'t be seen on the host side.
-         2: areturn
+         x: ldc           #x                 // String This value shouldn\'t be seen on the host side.
+         x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -626,9 +626,9 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokevirtual #x                 // Method unsupportedMethod:()Ljava/lang/String;
-         4: areturn
+         x: aload_0
+         x: invokevirtual #x                 // Method unsupportedMethod:()Ljava/lang/String;
+         x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -636,12 +636,12 @@
 }
 SourceFile: "TinyFrameworkClassClassWideAnnotations.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassStub
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkClassLoadHook.class
   Compiled from "TinyFrameworkClassLoadHook.java"
@@ -662,9 +662,9 @@
     flags: (0x0002) ACC_PRIVATE
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                 // Method java/lang/Object."<init>":()V
-         4: return
+         x: aload_0
+         x: invokespecial #x                 // Method java/lang/Object."<init>":()V
+         x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -675,11 +675,11 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=2, locals=1, args_size=1
-         0: getstatic     #x                 // Field sLoadedClasses:Ljava/util/Set;
-         3: aload_0
-         4: invokeinterface #x,  2           // InterfaceMethod java/util/Set.add:(Ljava/lang/Object;)Z
-         9: pop
-        10: return
+         x: getstatic     #x                 // Field sLoadedClasses:Ljava/util/Set;
+         x: aload_0
+         x: invokeinterface #x,  2           // InterfaceMethod java/util/Set.add:(Ljava/lang/Object;)Z
+         x: pop
+        x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -694,21 +694,21 @@
     flags: (0x0008) ACC_STATIC
     Code:
       stack=2, locals=0, args_size=0
-         0: new           #x                 // class java/util/HashSet
-         3: dup
-         4: invokespecial #x                 // Method java/util/HashSet."<init>":()V
-         7: putstatic     #x                 // Field sLoadedClasses:Ljava/util/Set;
-        10: return
+         x: new           #x                 // class java/util/HashSet
+         x: dup
+         x: invokespecial #x                 // Method java/util/HashSet."<init>":()V
+         x: putstatic     #x                 // Field sLoadedClasses:Ljava/util/Set;
+        x: return
       LineNumberTable:
 }
 SourceFile: "TinyFrameworkClassLoadHook.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassStub
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWithInitializer.class
   Compiled from "TinyFrameworkClassWithInitializer.java"
@@ -728,9 +728,9 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                 // Method java/lang/Object."<init>":()V
-         4: return
+         x: aload_0
+         x: invokespecial #x                 // Method java/lang/Object."<init>":()V
+         x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -741,26 +741,26 @@
     flags: (0x0008) ACC_STATIC
     Code:
       stack=2, locals=0, args_size=0
-         0: ldc           #x                  // class com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWithInitializer
-         2: ldc           #x                 // String com.android.hoststubgen.test.tinyframework.TinyFrameworkClassLoadHook.onClassLoaded
-         4: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
-         7: iconst_1
-         8: putstatic     #x                 // Field sInitialized:Z
-        11: return
+         x: ldc           #x                  // class com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWithInitializer
+         x: ldc           #x                 // String com.android.hoststubgen.test.tinyframework.TinyFrameworkClassLoadHook.onClassLoaded
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
+         x: iconst_1
+         x: putstatic     #x                 // Field sInitialized:Z
+        x: return
       LineNumberTable:
 }
 SourceFile: "TinyFrameworkClassWithInitializer.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 RuntimeInvisibleAnnotations:
-  0: #x(#x=s#x)
+  x: #x(#x=s#x)
     android.hosttest.annotation.HostSideTestClassLoadHook(
       value="com.android.hoststubgen.test.tinyframework.TinyFrameworkClassLoadHook.onClassLoaded"
     )
-  1: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassStub
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkExceptionTester.class
   Compiled from "TinyFrameworkExceptionTester.java"
@@ -776,9 +776,9 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                 // Method java/lang/Object."<init>":()V
-         4: return
+         x: aload_0
+         x: invokespecial #x                 // Method java/lang/Object."<init>":()V
+         x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -789,18 +789,18 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=4, locals=1, args_size=0
-         0: new           #x                 // class java/lang/IllegalStateException
-         3: dup
-         4: ldc           #x                 // String Inner exception
-         6: invokespecial #x                 // Method java/lang/IllegalStateException."<init>":(Ljava/lang/String;)V
-         9: athrow
-        10: astore_0
-        11: new           #x                 // class java/lang/RuntimeException
-        14: dup
-        15: ldc           #x                 // String Outer exception
-        17: aload_0
-        18: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;Ljava/lang/Throwable;)V
-        21: athrow
+         x: new           #x                 // class java/lang/IllegalStateException
+         x: dup
+         x: ldc           #x                 // String Inner exception
+         x: invokespecial #x                 // Method java/lang/IllegalStateException."<init>":(Ljava/lang/String;)V
+         x: athrow
+        x: astore_0
+        x: new           #x                 // class java/lang/RuntimeException
+        x: dup
+        x: ldc           #x                 // String Outer exception
+        x: aload_0
+        x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;Ljava/lang/Throwable;)V
+        x: athrow
       Exception table:
          from    to  target type
              0    10    10   Class java/lang/Exception
@@ -814,12 +814,12 @@
 }
 SourceFile: "TinyFrameworkExceptionTester.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassStub
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkForTextPolicy.class
   Compiled from "TinyFrameworkForTextPolicy.java"
@@ -843,25 +843,25 @@
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
     Code:
       stack=2, locals=0, args_size=0
-         0: ldc           #x                  // class com/android/hoststubgen/test/tinyframework/TinyFrameworkForTextPolicy
-         2: ldc           #x                 // String com.android.hoststubgen.test.tinyframework.TinyFrameworkClassLoadHook.onClassLoaded
-         4: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
-         7: return
+         x: ldc           #x                  // class com/android/hoststubgen/test/tinyframework/TinyFrameworkForTextPolicy
+         x: ldc           #x                 // String com.android.hoststubgen.test.tinyframework.TinyFrameworkClassLoadHook.onClassLoaded
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
+         x: return
 
   public com.android.hoststubgen.test.tinyframework.TinyFrameworkForTextPolicy();
     descriptor: ()V
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                 // Method java/lang/Object."<init>":()V
-         4: aload_0
-         5: iconst_1
-         6: putfield      #x                 // Field stub:I
-         9: aload_0
-        10: iconst_2
-        11: putfield      #x                 // Field keep:I
-        14: return
+         x: aload_0
+         x: invokespecial #x                 // Method java/lang/Object."<init>":()V
+         x: aload_0
+         x: iconst_1
+         x: putfield      #x                 // Field stub:I
+         x: aload_0
+        x: iconst_2
+        x: putfield      #x                 // Field keep:I
+        x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -872,10 +872,10 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=2, args_size=2
-         0: aload_0
-         1: iload_1
-         2: invokevirtual #x                 // Method addOneInner:(I)I
-         5: ireturn
+         x: aload_0
+         x: iload_1
+         x: invokevirtual #x                 // Method addOneInner:(I)I
+         x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -887,16 +887,16 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=4, locals=2, args_size=2
-         0: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkForTextPolicy
-         2: ldc           #x                 // String addOneInner
-         4: ldc           #x                 // String (I)I
-         6: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
-         9: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
-        12: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
-        15: iload_1
-        16: iconst_1
-        17: iadd
-        18: ireturn
+         x: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkForTextPolicy
+         x: ldc           #x                 // String addOneInner
+         x: ldc           #x                 // String (I)I
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
+         x: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
+        x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
+        x: iload_1
+        x: iconst_1
+        x: iadd
+        x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -908,10 +908,10 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=2, args_size=2
-         0: iload_1
-         1: iconst_2
-         2: iadd
-         3: ireturn
+         x: iload_1
+         x: iconst_2
+         x: iadd
+         x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -923,10 +923,10 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=2, locals=1, args_size=1
-         0: iload_0
-         1: iconst_3
-         2: iadd
-         3: ireturn
+         x: iload_0
+         x: iconst_3
+         x: iadd
+         x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -937,27 +937,27 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=4, locals=1, args_size=1
-         0: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkForTextPolicy
-         2: ldc           #x                 // String unsupportedMethod
-         4: ldc           #x                 // String ()Ljava/lang/String;
-         6: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
-         9: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
-        12: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
-        15: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onThrowMethodCalled:()V
-        18: new           #x                 // class java/lang/RuntimeException
-        21: dup
-        22: ldc           #x                 // String Unreachable
-        24: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
-        27: athrow
+         x: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkForTextPolicy
+         x: ldc           #x                 // String unsupportedMethod
+         x: ldc           #x                 // String ()Ljava/lang/String;
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
+         x: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
+        x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
+        x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onThrowMethodCalled:()V
+        x: new           #x                 // class java/lang/RuntimeException
+        x: dup
+        x: ldc           #x                 // String Unreachable
+        x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+        x: athrow
 
   public java.lang.String visibleButUsesUnsupportedMethod();
     descriptor: ()Ljava/lang/String;
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokevirtual #x                 // Method unsupportedMethod:()Ljava/lang/String;
-         4: areturn
+         x: aload_0
+         x: invokevirtual #x                 // Method unsupportedMethod:()Ljava/lang/String;
+         x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -965,9 +965,9 @@
 }
 SourceFile: "TinyFrameworkForTextPolicy.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkNative.class
   Compiled from "TinyFrameworkNative.java"
@@ -983,9 +983,9 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                 // Method java/lang/Object."<init>":()V
-         4: return
+         x: aload_0
+         x: invokespecial #x                 // Method java/lang/Object."<init>":()V
+         x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -996,18 +996,18 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=1, locals=1, args_size=1
-         0: iload_0
-         1: invokestatic  #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.nativeAddTwo:(I)I
-         4: ireturn
+         x: iload_0
+         x: invokestatic  #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.nativeAddTwo:(I)I
+         x: ireturn
 
   public static int nativeAddTwo_should_be_like_this(int);
     descriptor: (I)I
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=1, locals=1, args_size=1
-         0: iload_0
-         1: invokestatic  #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.nativeAddTwo:(I)I
-         4: ireturn
+         x: iload_0
+         x: invokestatic  #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.nativeAddTwo:(I)I
+         x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1018,20 +1018,20 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=4, locals=4, args_size=2
-         0: lload_0
-         1: lload_2
-         2: invokestatic  #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.nativeLongPlus:(JJ)J
-         5: lreturn
+         x: lload_0
+         x: lload_2
+         x: invokestatic  #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.nativeLongPlus:(JJ)J
+         x: lreturn
 
   public static long nativeLongPlus_should_be_like_this(long, long);
     descriptor: (JJ)J
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=4, locals=4, args_size=2
-         0: lload_0
-         1: lload_2
-         2: invokestatic  #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.nativeLongPlus:(JJ)J
-         5: lreturn
+         x: lload_0
+         x: lload_2
+         x: invokestatic  #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.nativeLongPlus:(JJ)J
+         x: lreturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1040,14 +1040,14 @@
 }
 SourceFile: "TinyFrameworkNative.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassStub
-  1: #x(#x=s#x)
+  x: #x(#x=s#x)
     android.hosttest.annotation.HostSideTestNativeSubstitutionClass(
       value="TinyFrameworkNative_host"
     )
@@ -1065,15 +1065,15 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=4, locals=1, args_size=1
-         0: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host
-         2: ldc           #x                 // String <init>
-         4: ldc           #x                 // String ()V
-         6: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
-         9: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
-        12: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
-        15: aload_0
-        16: invokespecial #x                 // Method java/lang/Object."<init>":()V
-        19: return
+         x: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host
+         x: ldc           #x                 // String <init>
+         x: ldc           #x                 // String ()V
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
+         x: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
+        x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
+        x: aload_0
+        x: invokespecial #x                 // Method java/lang/Object."<init>":()V
+        x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1084,16 +1084,16 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=4, locals=1, args_size=1
-         0: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host
-         2: ldc           #x                 // String nativeAddTwo
-         4: ldc           #x                 // String (I)I
-         6: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
-         9: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
-        12: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
-        15: iload_0
-        16: iconst_2
-        17: iadd
-        18: ireturn
+         x: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host
+         x: ldc           #x                 // String nativeAddTwo
+         x: ldc           #x                 // String (I)I
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
+         x: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
+        x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
+        x: iload_0
+        x: iconst_2
+        x: iadd
+        x: ireturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1104,16 +1104,16 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=4, locals=4, args_size=2
-         0: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host
-         2: ldc           #x                 // String nativeLongPlus
-         4: ldc           #x                 // String (JJ)J
-         6: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
-         9: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
-        12: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
-        15: lload_0
-        16: lload_2
-        17: ladd
-        18: lreturn
+         x: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host
+         x: ldc           #x                 // String nativeLongPlus
+         x: ldc           #x                 // String (JJ)J
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
+         x: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
+        x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
+        x: lload_0
+        x: lload_2
+        x: ladd
+        x: lreturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1122,10 +1122,10 @@
 }
 SourceFile: "TinyFrameworkNative_host.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassKeep
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$1.class
   Compiled from "TinyFrameworkNestedClasses.java"
@@ -1145,12 +1145,12 @@
     flags: (0x0000)
     Code:
       stack=2, locals=2, args_size=2
-         0: aload_0
-         1: aload_1
-         2: putfield      #x                 // Field this$0:Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses;
-         5: aload_0
-         6: invokespecial #x                 // Method java/lang/Object."<init>":()V
-         9: return
+         x: aload_0
+         x: aload_1
+         x: putfield      #x                 // Field this$0:Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses;
+         x: aload_0
+         x: invokespecial #x                 // Method java/lang/Object."<init>":()V
+         x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1162,15 +1162,15 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=4, locals=1, args_size=1
-         0: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$1
-         2: ldc           #x                 // String get
-         4: ldc           #x                 // String ()Ljava/lang/Integer;
-         6: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
-         9: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
-        12: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
-        15: iconst_1
-        16: invokestatic  #x                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
-        19: areturn
+         x: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$1
+         x: ldc           #x                 // String get
+         x: ldc           #x                 // String ()Ljava/lang/Integer;
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
+         x: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
+        x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
+        x: iconst_1
+        x: invokestatic  #x                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
+        x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1181,15 +1181,15 @@
     flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
     Code:
       stack=4, locals=1, args_size=1
-         0: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$1
-         2: ldc           #x                 // String get
-         4: ldc           #x                 // String ()Ljava/lang/Object;
-         6: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
-         9: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
-        12: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
-        15: aload_0
-        16: invokevirtual #x                 // Method get:()Ljava/lang/Integer;
-        19: areturn
+         x: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$1
+         x: ldc           #x                 // String get
+         x: ldc           #x                 // String ()Ljava/lang/Object;
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
+         x: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
+        x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
+        x: aload_0
+        x: invokevirtual #x                 // Method get:()Ljava/lang/Integer;
+        x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1201,7 +1201,7 @@
 Signature: #x                           // Ljava/lang/Object;Ljava/util/function/Supplier<Ljava/lang/Integer;>;
 SourceFile: "TinyFrameworkNestedClasses.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 NestHost: class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$2.class
@@ -1218,9 +1218,9 @@
     flags: (0x0000)
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                 // Method java/lang/Object."<init>":()V
-         4: return
+         x: aload_0
+         x: invokespecial #x                 // Method java/lang/Object."<init>":()V
+         x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1231,15 +1231,15 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=4, locals=1, args_size=1
-         0: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$2
-         2: ldc           #x                 // String get
-         4: ldc           #x                 // String ()Ljava/lang/Integer;
-         6: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
-         9: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
-        12: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
-        15: iconst_2
-        16: invokestatic  #x                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
-        19: areturn
+         x: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$2
+         x: ldc           #x                 // String get
+         x: ldc           #x                 // String ()Ljava/lang/Integer;
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
+         x: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
+        x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
+        x: iconst_2
+        x: invokestatic  #x                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
+        x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1250,15 +1250,15 @@
     flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
     Code:
       stack=4, locals=1, args_size=1
-         0: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$2
-         2: ldc           #x                 // String get
-         4: ldc           #x                 // String ()Ljava/lang/Object;
-         6: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
-         9: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
-        12: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
-        15: aload_0
-        16: invokevirtual #x                 // Method get:()Ljava/lang/Integer;
-        19: areturn
+         x: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$2
+         x: ldc           #x                 // String get
+         x: ldc           #x                 // String ()Ljava/lang/Object;
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
+         x: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
+        x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
+        x: aload_0
+        x: invokevirtual #x                 // Method get:()Ljava/lang/Integer;
+        x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1270,7 +1270,7 @@
 Signature: #x                           // Ljava/lang/Object;Ljava/util/function/Supplier<Ljava/lang/Integer;>;
 SourceFile: "TinyFrameworkNestedClasses.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 NestHost: class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$3.class
@@ -1291,12 +1291,12 @@
     flags: (0x0000)
     Code:
       stack=2, locals=2, args_size=2
-         0: aload_0
-         1: aload_1
-         2: putfield      #x                 // Field this$0:Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses;
-         5: aload_0
-         6: invokespecial #x                 // Method java/lang/Object."<init>":()V
-         9: return
+         x: aload_0
+         x: aload_1
+         x: putfield      #x                 // Field this$0:Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses;
+         x: aload_0
+         x: invokespecial #x                 // Method java/lang/Object."<init>":()V
+         x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1308,15 +1308,15 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=4, locals=1, args_size=1
-         0: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$3
-         2: ldc           #x                 // String get
-         4: ldc           #x                 // String ()Ljava/lang/Integer;
-         6: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
-         9: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
-        12: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
-        15: iconst_3
-        16: invokestatic  #x                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
-        19: areturn
+         x: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$3
+         x: ldc           #x                 // String get
+         x: ldc           #x                 // String ()Ljava/lang/Integer;
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
+         x: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
+        x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
+        x: iconst_3
+        x: invokestatic  #x                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
+        x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1327,15 +1327,15 @@
     flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
     Code:
       stack=4, locals=1, args_size=1
-         0: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$3
-         2: ldc           #x                 // String get
-         4: ldc           #x                 // String ()Ljava/lang/Object;
-         6: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
-         9: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
-        12: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
-        15: aload_0
-        16: invokevirtual #x                 // Method get:()Ljava/lang/Integer;
-        19: areturn
+         x: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$3
+         x: ldc           #x                 // String get
+         x: ldc           #x                 // String ()Ljava/lang/Object;
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
+         x: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
+        x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
+        x: aload_0
+        x: invokevirtual #x                 // Method get:()Ljava/lang/Integer;
+        x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1347,7 +1347,7 @@
 Signature: #x                           // Ljava/lang/Object;Ljava/util/function/Supplier<Ljava/lang/Integer;>;
 SourceFile: "TinyFrameworkNestedClasses.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 NestHost: class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$4.class
@@ -1364,9 +1364,9 @@
     flags: (0x0000)
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                 // Method java/lang/Object."<init>":()V
-         4: return
+         x: aload_0
+         x: invokespecial #x                 // Method java/lang/Object."<init>":()V
+         x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1377,15 +1377,15 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=4, locals=1, args_size=1
-         0: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$4
-         2: ldc           #x                 // String get
-         4: ldc           #x                 // String ()Ljava/lang/Integer;
-         6: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
-         9: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
-        12: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
-        15: iconst_4
-        16: invokestatic  #x                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
-        19: areturn
+         x: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$4
+         x: ldc           #x                 // String get
+         x: ldc           #x                 // String ()Ljava/lang/Integer;
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
+         x: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
+        x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
+        x: iconst_4
+        x: invokestatic  #x                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
+        x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1396,15 +1396,15 @@
     flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
     Code:
       stack=4, locals=1, args_size=1
-         0: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$4
-         2: ldc           #x                 // String get
-         4: ldc           #x                 // String ()Ljava/lang/Object;
-         6: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
-         9: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
-        12: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
-        15: aload_0
-        16: invokevirtual #x                 // Method get:()Ljava/lang/Integer;
-        19: areturn
+         x: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$4
+         x: ldc           #x                 // String get
+         x: ldc           #x                 // String ()Ljava/lang/Object;
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
+         x: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
+        x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
+        x: aload_0
+        x: invokevirtual #x                 // Method get:()Ljava/lang/Integer;
+        x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1416,7 +1416,7 @@
 Signature: #x                           // Ljava/lang/Object;Ljava/util/function/Supplier<Ljava/lang/Integer;>;
 SourceFile: "TinyFrameworkNestedClasses.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 NestHost: class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$BaseClass.class
@@ -1437,12 +1437,12 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=2, args_size=2
-         0: aload_0
-         1: invokespecial #x                 // Method java/lang/Object."<init>":()V
-         4: aload_0
-         5: iload_1
-         6: putfield      #x                 // Field value:I
-         9: return
+         x: aload_0
+         x: invokespecial #x                 // Method java/lang/Object."<init>":()V
+         x: aload_0
+         x: iload_1
+         x: putfield      #x                 // Field value:I
+         x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1453,9 +1453,9 @@
   public static #x= #x of #x;            // BaseClass=class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$BaseClass of class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses
 SourceFile: "TinyFrameworkNestedClasses.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 NestHost: class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$InnerClass.class
@@ -1480,15 +1480,15 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=2, args_size=2
-         0: aload_0
-         1: aload_1
-         2: putfield      #x                 // Field this$0:Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses;
-         5: aload_0
-         6: invokespecial #x                 // Method java/lang/Object."<init>":()V
-         9: aload_0
-        10: iconst_5
-        11: putfield      #x                 // Field value:I
-        14: return
+         x: aload_0
+         x: aload_1
+         x: putfield      #x                 // Field this$0:Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses;
+         x: aload_0
+         x: invokespecial #x                 // Method java/lang/Object."<init>":()V
+         x: aload_0
+        x: iconst_5
+        x: putfield      #x                 // Field value:I
+        x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1499,12 +1499,12 @@
   public #x= #x of #x;                   // InnerClass=class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$InnerClass of class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses
 SourceFile: "TinyFrameworkNestedClasses.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassStub
 NestHost: class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$StaticNestedClass$1.class
@@ -1521,9 +1521,9 @@
     flags: (0x0000)
     Code:
       stack=1, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                 // Method java/lang/Object."<init>":()V
-         4: return
+         x: aload_0
+         x: invokespecial #x                 // Method java/lang/Object."<init>":()V
+         x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1534,15 +1534,15 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=4, locals=1, args_size=1
-         0: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$StaticNestedClass$1
-         2: ldc           #x                 // String get
-         4: ldc           #x                 // String ()Ljava/lang/Integer;
-         6: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
-         9: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
-        12: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
-        15: bipush        7
-        17: invokestatic  #x                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
-        20: areturn
+         x: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$StaticNestedClass$1
+         x: ldc           #x                 // String get
+         x: ldc           #x                 // String ()Ljava/lang/Integer;
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
+         x: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
+        x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
+        x: bipush        7
+        x: invokestatic  #x                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
+        x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1553,15 +1553,15 @@
     flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
     Code:
       stack=4, locals=1, args_size=1
-         0: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$StaticNestedClass$1
-         2: ldc           #x                 // String get
-         4: ldc           #x                 // String ()Ljava/lang/Object;
-         6: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
-         9: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
-        12: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
-        15: aload_0
-        16: invokevirtual #x                 // Method get:()Ljava/lang/Integer;
-        19: areturn
+         x: ldc           #x                 // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$StaticNestedClass$1
+         x: ldc           #x                 // String get
+         x: ldc           #x                 // String ()Ljava/lang/Object;
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
+         x: invokevirtual #x                 // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
+        x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
+        x: aload_0
+        x: invokevirtual #x                 // Method get:()Ljava/lang/Integer;
+        x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1574,7 +1574,7 @@
 Signature: #x                           // Ljava/lang/Object;Ljava/util/function/Supplier<Ljava/lang/Integer;>;
 SourceFile: "TinyFrameworkNestedClasses.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 NestHost: class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$StaticNestedClass.class
@@ -1595,12 +1595,12 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                 // Method java/lang/Object."<init>":()V
-         4: aload_0
-         5: bipush        6
-         7: putfield      #x                 // Field value:I
-        10: return
+         x: aload_0
+         x: invokespecial #x                 // Method java/lang/Object."<init>":()V
+         x: aload_0
+         x: bipush        6
+         x: putfield      #x                 // Field value:I
+        x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1611,10 +1611,10 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=2, locals=0, args_size=0
-         0: new           #x                 // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$StaticNestedClass$1
-         3: dup
-         4: invokespecial #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$StaticNestedClass$1."<init>":()V
-         7: areturn
+         x: new           #x                 // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$StaticNestedClass$1
+         x: dup
+         x: invokespecial #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$StaticNestedClass$1."<init>":()V
+         x: areturn
       LineNumberTable:
     Signature: #x                          // ()Ljava/util/function/Supplier<Ljava/lang/Integer;>;
 }
@@ -1623,12 +1623,12 @@
   #x;                                    // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$StaticNestedClass$1
 SourceFile: "TinyFrameworkNestedClasses.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassStub
 NestHost: class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$SubClass.class
@@ -1645,10 +1645,10 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=2, locals=2, args_size=2
-         0: aload_0
-         1: iload_1
-         2: invokespecial #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$BaseClass."<init>":(I)V
-         5: return
+         x: aload_0
+         x: iload_1
+         x: invokespecial #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$BaseClass."<init>":(I)V
+         x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1660,9 +1660,9 @@
   public static #x= #x of #x;            // SubClass=class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$SubClass of class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses
 SourceFile: "TinyFrameworkNestedClasses.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 NestHost: class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses.class
@@ -1689,15 +1689,15 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=4, locals=1, args_size=1
-         0: aload_0
-         1: invokespecial #x                 // Method java/lang/Object."<init>":()V
-         4: aload_0
-         5: new           #x                 // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$1
-         8: dup
-         9: aload_0
-        10: invokespecial #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$1."<init>":(Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses;)V
-        13: putfield      #x                 // Field mSupplier:Ljava/util/function/Supplier;
-        16: return
+         x: aload_0
+         x: invokespecial #x                 // Method java/lang/Object."<init>":()V
+         x: aload_0
+         x: new           #x                 // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$1
+         x: dup
+         x: aload_0
+        x: invokespecial #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$1."<init>":(Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses;)V
+        x: putfield      #x                 // Field mSupplier:Ljava/util/function/Supplier;
+        x: return
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1708,11 +1708,11 @@
     flags: (0x0001) ACC_PUBLIC
     Code:
       stack=3, locals=1, args_size=1
-         0: new           #x                 // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$3
-         3: dup
-         4: aload_0
-         5: invokespecial #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$3."<init>":(Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses;)V
-         8: areturn
+         x: new           #x                 // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$3
+         x: dup
+         x: aload_0
+         x: invokespecial #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$3."<init>":(Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses;)V
+         x: areturn
       LineNumberTable:
       LocalVariableTable:
         Start  Length  Slot  Name   Signature
@@ -1724,10 +1724,10 @@
     flags: (0x0009) ACC_PUBLIC, ACC_STATIC
     Code:
       stack=2, locals=0, args_size=0
-         0: new           #x                 // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$4
-         3: dup
-         4: invokespecial #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$4."<init>":()V
-         7: areturn
+         x: new           #x                 // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$4
+         x: dup
+         x: invokespecial #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$4."<init>":()V
+         x: areturn
       LineNumberTable:
     Signature: #x                          // ()Ljava/util/function/Supplier<Ljava/lang/Integer;>;
 
@@ -1736,11 +1736,11 @@
     flags: (0x0008) ACC_STATIC
     Code:
       stack=2, locals=0, args_size=0
-         0: new           #x                 // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$2
-         3: dup
-         4: invokespecial #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$2."<init>":()V
-         7: putstatic     #x                 // Field sSupplier:Ljava/util/function/Supplier;
-        10: return
+         x: new           #x                 // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$2
+         x: dup
+         x: invokespecial #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$2."<init>":()V
+         x: putstatic     #x                 // Field sSupplier:Ljava/util/function/Supplier;
+        x: return
       LineNumberTable:
 }
 InnerClasses:
@@ -1755,12 +1755,12 @@
   #x;                                    // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$StaticNestedClass$1
 SourceFile: "TinyFrameworkNestedClasses.java"
 RuntimeVisibleAnnotations:
-  0: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedStubClass
-  1: #x()
+  x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedKeepClass
 RuntimeInvisibleAnnotations:
-  0: #x()
+  x: #x()
     android.hosttest.annotation.HostSideTestWholeClassStub
 NestMembers:
   com/android/hoststubgen/test/tinyframework/TinyFrameworkNestedClasses$SubClass
diff --git a/tools/hoststubgen/scripts/dump-jar b/tools/hoststubgen/scripts/dump-jar
index 93729fb..992665e 100755
--- a/tools/hoststubgen/scripts/dump-jar
+++ b/tools/hoststubgen/scripts/dump-jar
@@ -93,6 +93,7 @@
   if (( $simple )) ; then
     # For "simple output" mode,
     # - Normalize the constant numbers (replace with "#x")
+    # - Normalize byte code offsets and other similar numbers. (e.g. "0:" -> "x:")
     # - Remove the constant pool
     # - Remove the line number table
     # - Some other transient lines
@@ -100,6 +101,7 @@
     # `/PATTERN-1/,/PATTERN-1/{//!d}` is a trick to delete lines between two patterns, without
     # the start and the end lines.
     sed -e 's/#[0-9][0-9]*/#x/g' \
+        -e 's/^\( *\)[0-9][0-9]*:/\1x:/' \
         -e '/^Constant pool:/,/^[^ ]/{//!d}' \
         -e '/^ *line *[0-9][0-9]*: *[0-9][0-9]*$/d' \
         -e '/SHA-256 checksum/d' \
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))