Merge "Update javadocs for OVERRIDE_MIN_ASPECT_RATIO" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index db51537..e3cbd92 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -14,12 +14,14 @@
 
 aconfig_srcjars = [
     ":android.app.usage.flags-aconfig-java{.generated_srcjars}",
+    ":android.app.smartspace.flags-aconfig-java{.generated_srcjars}",
     ":android.companion.flags-aconfig-java{.generated_srcjars}",
     ":android.content.pm.flags-aconfig-java{.generated_srcjars}",
     ":android.content.res.flags-aconfig-java{.generated_srcjars}",
     ":android.hardware.flags-aconfig-java{.generated_srcjars}",
     ":android.hardware.radio.flags-aconfig-java{.generated_srcjars}",
     ":android.location.flags-aconfig-java{.generated_srcjars}",
+    ":android.net.vcn.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}",
@@ -567,6 +569,19 @@
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
 
+// VCN
+aconfig_declarations {
+    name: "android.net.vcn.flags-aconfig",
+    package: "android.net.vcn",
+    srcs: ["core/java/android/net/vcn/*.aconfig"],
+}
+
+java_aconfig_library {
+    name: "android.net.vcn.flags-aconfig-java",
+    aconfig_declarations: "android.net.vcn.flags-aconfig",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
 // DevicePolicy
 aconfig_declarations {
     name: "device_policy_aconfig_flags",
@@ -599,3 +614,16 @@
     aconfig_declarations: "android.service.notification.flags-aconfig",
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
+
+// Smartspace
+aconfig_declarations {
+    name: "android.app.smartspace.flags-aconfig",
+    package: "android.app.smartspace.flags",
+    srcs: ["core/java/android/app/smartspace/flags.aconfig"],
+}
+
+java_aconfig_library {
+    name: "android.app.smartspace.flags-aconfig-java",
+    aconfig_declarations: "android.app.smartspace.flags-aconfig",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
diff --git a/Android.bp b/Android.bp
index 7d5e788..b5f7e99 100644
--- a/Android.bp
+++ b/Android.bp
@@ -614,30 +614,6 @@
     ],
 }
 
-// TODO(b/145644363): move this to under StubLibraries.bp or ApiDocs.bp
-metalava_framework_docs_args = "" +
-    "--api-lint-ignore-prefix android.icu. " +
-    "--api-lint-ignore-prefix java. " +
-    "--api-lint-ignore-prefix junit. " +
-    "--api-lint-ignore-prefix org. " +
-    "--error NoSettingsProvider " +
-    "--error UnhiddenSystemApi " +
-    "--error UnflaggedApi " +
-    "--force-convert-to-warning-nullability-annotations +*:-android.*:+android.icu.*:-dalvik.* " +
-    "--hide BroadcastBehavior " +
-    "--hide CallbackInterface " +
-    "--hide DeprecationMismatch " +
-    "--hide HiddenSuperclass " +
-    "--hide HiddenTypeParameter " +
-    "--hide MissingPermission " +
-    "--hide-package android.audio.policy.configuration.V7_0 " +
-    "--hide-package com.android.server " +
-    "--hide RequiresPermission " +
-    "--hide SdkConstant " +
-    "--hide Todo " +
-    "--hide UnavailableSymbol " +
-    "--manifest $(location :frameworks-base-core-AndroidManifest.xml) "
-
 packages_to_document = [
     "android",
     "dalvik",
@@ -706,6 +682,27 @@
         "android.hardware.vibrator-V1.3-java",
         "framework-protos",
     ],
+    flags: [
+        "--api-lint-ignore-prefix android.icu.",
+        "--api-lint-ignore-prefix java.",
+        "--api-lint-ignore-prefix junit.",
+        "--api-lint-ignore-prefix org.",
+        "--error NoSettingsProvider",
+        "--error UnhiddenSystemApi",
+        "--error UnflaggedApi",
+        "--force-convert-to-warning-nullability-annotations +*:-android.*:+android.icu.*:-dalvik.*",
+        "--hide BroadcastBehavior",
+        "--hide CallbackInterface",
+        "--hide DeprecationMismatch",
+        "--hide HiddenSuperclass",
+        "--hide MissingPermission",
+        "--hide RequiresPermission",
+        "--hide SdkConstant",
+        "--hide Todo",
+        "--hide-package android.audio.policy.configuration.V7_0",
+        "--hide-package com.android.server",
+        "--manifest $(location :frameworks-base-core-AndroidManifest.xml)",
+    ],
     filter_packages: packages_to_document,
     high_mem: true, // Lots of sources => high memory use, see b/170701554
     installable: false,
diff --git a/apex/jobscheduler/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig
index 3e835b8..eb5502b 100644
--- a/apex/jobscheduler/service/aconfig/job.aconfig
+++ b/apex/jobscheduler/service/aconfig/job.aconfig
@@ -3,6 +3,6 @@
 flag {
     name: "relax_prefetch_connectivity_constraint_only_on_charger"
     namespace: "backstage_power"
-    description: "Only relax a prefetch job's connectivity constraint when the device is charging"
+    description: "Only relax a prefetch job's connectivity constraint when the device is charging and battery is not low"
     bug: "299329948"
 }
\ No newline at end of file
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
index 45f15db..63eaa63 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
@@ -932,7 +932,9 @@
             return false;
         }
         if (relaxPrefetchConnectivityConstraintOnlyOnCharger()) {
-            if (!mService.isBatteryCharging()) {
+            // Since the constraint relaxation isn't required by the job, only do it when the
+            // device is charging and the battery level is above the "low battery" threshold.
+            if (!mService.isBatteryCharging() || !mService.isBatteryNotLow()) {
                 return false;
             }
         }
diff --git a/api/ApiDocs.bp b/api/ApiDocs.bp
index e086bfe..5744bdf 100644
--- a/api/ApiDocs.bp
+++ b/api/ApiDocs.bp
@@ -72,7 +72,6 @@
         "android-non-updatable-doc-stubs-defaults",
         "module-classpath-stubs-defaults",
     ],
-    args: metalava_framework_docs_args,
 }
 
 droidstubs {
@@ -81,8 +80,7 @@
         "android-non-updatable-doc-stubs-defaults",
         "module-classpath-stubs-defaults",
     ],
-    args: metalava_framework_docs_args +
-        " --show-annotation android.annotation.SystemApi\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\) ",
+    flags: ["--show-annotation android.annotation.SystemApi\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\)"],
 }
 
 droidstubs {
@@ -91,9 +89,10 @@
         "android-non-updatable-doc-stubs-defaults",
         "module-classpath-stubs-defaults",
     ],
-    args: metalava_framework_docs_args +
-        " --show-annotation android.annotation.SystemApi\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\) " +
-        " --show-annotation android.annotation.SystemApi\\(client=android.annotation.SystemApi.Client.MODULE_LIBRARIES\\) ",
+    flags: [
+        "--show-annotation android.annotation.SystemApi\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\)",
+        "--show-annotation android.annotation.SystemApi\\(client=android.annotation.SystemApi.Client.MODULE_LIBRARIES\\)",
+    ],
     generate_stubs: false, // We're only using this module for the annotations.zip output, disable doc-stubs.
     write_sdk_values: false,
 }
@@ -104,10 +103,11 @@
         "android-non-updatable-doc-stubs-defaults",
         "module-classpath-stubs-defaults",
     ],
-    args: metalava_framework_docs_args +
-        " --show-annotation android.annotation.SystemApi\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\) " +
-        " --show-annotation android.annotation.SystemApi\\(client=android.annotation.SystemApi.Client.MODULE_LIBRARIES\\) " +
-        " --show-annotation android.annotation.SystemApi\\(client=android.annotation.SystemApi.Client.SYSTEM_SERVER\\) ",
+    flags: [
+        "--show-annotation android.annotation.SystemApi\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\)",
+        "--show-annotation android.annotation.SystemApi\\(client=android.annotation.SystemApi.Client.MODULE_LIBRARIES\\)",
+        "--show-annotation android.annotation.SystemApi\\(client=android.annotation.SystemApi.Client.SYSTEM_SERVER\\)",
+    ],
     generate_stubs: false, // We're only using this module for the annotations.zip output, disable doc-stubs.
     write_sdk_values: false,
 }
@@ -116,7 +116,6 @@
     name: "framework-doc-stubs",
     defaults: ["android-non-updatable-doc-stubs-defaults"],
     srcs: [":all-modules-public-stubs-source"],
-    args: metalava_framework_docs_args,
     api_levels_module: "api_versions_public",
     aidl: {
         include_dirs: [
@@ -129,8 +128,7 @@
 droidstubs {
     name: "framework-doc-system-stubs",
     defaults: ["framework-doc-stubs-sources-default"],
-    args: metalava_framework_docs_args +
-        " --show-annotation android.annotation.SystemApi\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\) ",
+    flags: ["--show-annotation android.annotation.SystemApi\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\)"],
     api_levels_module: "api_versions_system",
 }
 
@@ -139,30 +137,6 @@
 // using droiddoc
 /////////////////////////////////////////////////////////////////////
 
-// doclava contains checks for a few issues that are have been migrated to metalava.
-// disable them in doclava, to avoid mistriggering or double triggering.
-ignore_doclava_errors_checked_by_metalava = "" +
-    "-hide 111 " + // HIDDEN_SUPERCLASS
-    "-hide 113 " + // DEPRECATION_MISMATCH
-    "-hide 125 " + // REQUIRES_PERMISSION
-    "-hide 126 " + // BROADCAST_BEHAVIOR
-    "-hide 127 " + // SDK_CONSTANT
-    "-hide 128 " // TODO
-
-framework_docs_only_args = "-android " +
-    "-manifest $(location :frameworks-base-core-AndroidManifest.xml) " +
-    "-metalavaApiSince " +
-    "-werror " +
-    "-lerror " +
-    ignore_doclava_errors_checked_by_metalava +
-    "-overview $(location :frameworks-base-java-overview) " +
-    // Federate Support Library references against local API file.
-    "-federate SupportLib https://developer.android.com " +
-    "-federationapi SupportLib $(location :current-support-api) " +
-    // Federate Support Library references against local API file.
-    "-federate AndroidX https://developer.android.com " +
-    "-federationapi AndroidX $(location :current-androidx-api) "
-
 doc_defaults {
     name: "framework-docs-default",
     sdk_version: "none",
@@ -182,6 +156,28 @@
     resourcesdir: "docs/html/reference/images/",
     resourcesoutdir: "reference/android/images/",
     lint_baseline: "javadoc-lint-baseline",
+    flags: [
+        "-android",
+        "-manifest $(location :frameworks-base-core-AndroidManifest.xml)",
+        "-metalavaApiSince",
+        "-werror",
+        "-lerror",
+        "-overview $(location :frameworks-base-java-overview)",
+        // Federate Support Library references against local API file.
+        "-federate SupportLib https://developer.android.com",
+        "-federationapi SupportLib $(location :current-support-api)",
+        // Federate Support Library references against local API file.
+        "-federate AndroidX https://developer.android.com",
+        "-federationapi AndroidX $(location :current-androidx-api)",
+        // doclava contains checks for a few issues that are have been migrated to metalava.
+        // disable them in doclava, to avoid mistriggering or double triggering.
+        "-hide 111", // HIDDEN_SUPERCLASS
+        "-hide 113", // DEPRECATION_MISMATCH
+        "-hide 125", // REQUIRES_PERMISSION
+        "-hide 126", // BROADCAST_BEHAVIOR
+        "-hide 127", // SDK_CONSTANT
+        "-hide 128", // TODO
+    ],
     hdf: [
         "dac true",
         "sdk.codename O",
@@ -217,7 +213,10 @@
     ],
     compat_config: ":global-compat-config",
     proofread_file: "offline-sdk-docs-proofread.txt",
-    args: framework_docs_only_args + " -offlinemode -title \"Android SDK\"",
+    flags: [
+        "-offlinemode",
+        "-title \"Android SDK\"",
+    ],
     static_doc_index_redirect: "docs/docs-preview-index.html",
 }
 
@@ -234,7 +233,11 @@
         "android.whichdoc offline",
     ],
     proofread_file: "offline-sdk-referenceonly-docs-proofread.txt",
-    args: framework_docs_only_args + " -offlinemode -title \"Android SDK\" -referenceonly",
+    flags: [
+        "-offlinemode",
+        "-title \"Android SDK\"",
+        "-referenceonly",
+    ],
     static_doc_index_redirect: "docs/docs-documentation-redirect.html",
     static_doc_properties: "docs/source.properties",
 }
@@ -252,8 +255,14 @@
         "android.whichdoc offline",
     ],
     proofread_file: "offline-system-sdk-referenceonly-docs-proofread.txt",
-    args: framework_docs_only_args + " -hide 101 -hide 104 -hide 108" +
-    " -offlinemode -title \"Android System SDK\" -referenceonly",
+    flags: [
+        "-hide 101",
+        "-hide 104",
+        "-hide 108",
+        "-offlinemode",
+        "-title \"Android System SDK\"",
+        "-referenceonly",
+    ],
     static_doc_index_redirect: "docs/docs-documentation-redirect.html",
     static_doc_properties: "docs/source.properties",
 }
@@ -269,22 +278,28 @@
         "android.hasSamples true",
     ],
     proofread_file: "ds-docs-proofread.txt",
-    args: framework_docs_only_args +
-        " -toroot / -yamlV2 -samplegroup Admin " +
-        " -samplegroup Background " +
-        " -samplegroup Connectivity " +
-        " -samplegroup Content " +
-        " -samplegroup Input " +
-        " -samplegroup Media " +
-        " -samplegroup Notification " +
-        " -samplegroup RenderScript " +
-        " -samplegroup Security " +
-        " -samplegroup Sensors " +
-        " -samplegroup System " +
-        " -samplegroup Testing " +
-        " -samplegroup UI " +
-        " -samplegroup Views " +
-        " -samplegroup Wearable -devsite -samplesdir development/samples/browseable ",
+    flags: [
+        " -toroot /",
+        "-yamlV2",
+        "-samplegroup Admin",
+        "-samplegroup Background",
+        "-samplegroup Connectivity",
+        "-samplegroup Content",
+        "-samplegroup Input",
+        "-samplegroup Media",
+        "-samplegroup Notification",
+        "-samplegroup RenderScript",
+        "-samplegroup Security",
+        "-samplegroup Sensors",
+        "-samplegroup System",
+        "-samplegroup Testing",
+        "-samplegroup UI",
+        "-samplegroup Views",
+        "-samplegroup Wearable",
+        "-devsite",
+        "-samplesdir",
+        "development/samples/browseable",
+    ],
 }
 
 droiddoc {
@@ -292,8 +307,11 @@
     srcs: [
         ":framework-doc-stubs",
     ],
-    args: "-noJdkLink -links https://kotlinlang.org/api/latest/jvm/stdlib/^external/dokka/package-list " +
+    flags: [
+        "-noJdkLink",
+        "-links https://kotlinlang.org/api/latest/jvm/stdlib/^external/dokka/package-list",
         "-noStdlibLink",
+    ],
     proofread_file: "ds-dokka-proofread.txt",
     dokka_enabled: true,
 }
@@ -346,11 +364,12 @@
     hdf: [
         "android.whichdoc online",
     ],
-    args: framework_docs_only_args +
-        " -staticonly " +
-        " -toroot / " +
-        " -devsite " +
-        " -ignoreJdLinks ",
+    flags: [
+        "-staticonly",
+        "-toroot /",
+        "-devsite",
+        "-ignoreJdLinks",
+    ],
 }
 
 droiddoc {
@@ -362,8 +381,9 @@
     hdf: [
         "android.whichdoc online",
     ],
-    args: framework_docs_only_args +
-        " -toroot / " +
-        " -atLinksNavtree " +
-        " -navtreeonly ",
+    flags: [
+        "-toroot /",
+        "-atLinksNavtree",
+        "-navtreeonly",
+    ],
 }
diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp
index 7e78185..d566552 100644
--- a/api/StubLibraries.bp
+++ b/api/StubLibraries.bp
@@ -36,7 +36,6 @@
         "android-non-updatable-stubs-defaults",
         "module-classpath-stubs-defaults",
     ],
-    args: metalava_framework_docs_args,
     check_api: {
         current: {
             api_file: ":non-updatable-current.txt",
@@ -70,19 +69,25 @@
     api_surface: "public",
 }
 
-priv_apps = " --show-annotation android.annotation.SystemApi\\(" +
-    "client=android.annotation.SystemApi.Client.PRIVILEGED_APPS" +
-    "\\)"
+priv_apps = [
+    "--show-annotation android.annotation.SystemApi\\(" +
+        "client=android.annotation.SystemApi.Client.PRIVILEGED_APPS" +
+        "\\)",
+]
 
-priv_apps_in_stubs = " --show-for-stub-purposes-annotation android.annotation.SystemApi\\(" +
-    "client=android.annotation.SystemApi.Client.PRIVILEGED_APPS" +
-    "\\)"
+priv_apps_in_stubs = [
+    "--show-for-stub-purposes-annotation android.annotation.SystemApi\\(" +
+        "client=android.annotation.SystemApi.Client.PRIVILEGED_APPS" +
+        "\\)",
+]
 
-test = " --show-annotation android.annotation.TestApi"
+test = ["--show-annotation android.annotation.TestApi"]
 
-module_libs = " --show-annotation android.annotation.SystemApi\\(" +
-    "client=android.annotation.SystemApi.Client.MODULE_LIBRARIES" +
-    "\\)"
+module_libs = [
+    "--show-annotation android.annotation.SystemApi\\(" +
+        "client=android.annotation.SystemApi.Client.MODULE_LIBRARIES" +
+        "\\)",
+]
 
 droidstubs {
     name: "system-api-stubs-docs-non-updatable",
@@ -93,7 +98,7 @@
         "android-non-updatable-stubs-defaults",
         "module-classpath-stubs-defaults",
     ],
-    args: metalava_framework_docs_args + priv_apps,
+    flags: priv_apps,
     check_api: {
         current: {
             api_file: ":non-updatable-system-current.txt",
@@ -136,7 +141,7 @@
         "android-non-updatable-stubs-defaults",
         "module-classpath-stubs-defaults",
     ],
-    args: metalava_framework_docs_args + test + priv_apps_in_stubs,
+    flags: test + priv_apps_in_stubs,
     check_api: {
         current: {
             api_file: ":non-updatable-test-current.txt",
@@ -186,7 +191,7 @@
         "android-non-updatable-stubs-defaults",
         "module-classpath-stubs-defaults",
     ],
-    args: metalava_framework_docs_args + priv_apps_in_stubs + module_libs,
+    flags: priv_apps_in_stubs + module_libs,
     check_api: {
         current: {
             api_file: ":non-updatable-module-lib-current.txt",
@@ -972,7 +977,7 @@
     merge_annotations_dirs: [
         "metalava-manual",
     ],
-    args: priv_apps,
+    flags: priv_apps,
 }
 
 java_library {
diff --git a/api/gen_combined_removed_dex.sh b/api/gen_combined_removed_dex.sh
index 71f366a..e0153f7 100755
--- a/api/gen_combined_removed_dex.sh
+++ b/api/gen_combined_removed_dex.sh
@@ -6,6 +6,6 @@
 
 # Convert each removed.txt to the "dex format" equivalent, and print all output.
 for f in "$@"; do
-    "$metalava_path" "$f" --dex-api "${tmp_dir}/tmp"
+    "$metalava_path" signature-to-dex "$f" "${tmp_dir}/tmp"
     cat "${tmp_dir}/tmp"
 done
diff --git a/core/api/current.txt b/core/api/current.txt
index 177352f..cce8329 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -13322,9 +13322,12 @@
 
   public final class SigningInfo implements android.os.Parcelable {
     ctor public SigningInfo();
+    ctor @FlaggedApi("android.content.pm.archiving") public SigningInfo(@IntRange(from=0) int, @Nullable java.util.Collection<android.content.pm.Signature>, @Nullable java.util.Collection<java.security.PublicKey>, @Nullable java.util.Collection<android.content.pm.Signature>);
     ctor public SigningInfo(android.content.pm.SigningInfo);
     method public int describeContents();
     method public android.content.pm.Signature[] getApkContentsSigners();
+    method @FlaggedApi("android.content.pm.archiving") @NonNull public java.util.Collection<java.security.PublicKey> getPublicKeys();
+    method @FlaggedApi("android.content.pm.archiving") @IntRange(from=0) public int getSchemeVersion();
     method public android.content.pm.Signature[] getSigningCertificateHistory();
     method public boolean hasMultipleSigners();
     method public boolean hasPastSigningCertificates();
@@ -33050,7 +33053,7 @@
   public static class PerformanceHintManager.Session implements java.io.Closeable {
     method public void close();
     method public void reportActualWorkDuration(long);
-    method public void setPreferPowerEfficiency(boolean);
+    method @FlaggedApi("android.os.adpf_prefer_power_efficiency") public void setPreferPowerEfficiency(boolean);
     method public void setThreads(@NonNull int[]);
     method public void updateTargetWorkDuration(long);
   }
@@ -44504,7 +44507,7 @@
     method public static final boolean isStartsPostDial(char);
     method public static boolean isVoiceMailNumber(String);
     method public static boolean isWellFormedSmsAddress(String);
-    method public static boolean isWpsCallNumber(@Nullable String);
+    method @FlaggedApi("com.android.internal.telephony.flags.enable_wps_check_api_flag") public static boolean isWpsCallNumber(@NonNull String);
     method public static byte[] networkPortionToCalledPartyBCD(String);
     method public static byte[] networkPortionToCalledPartyBCDWithLength(String);
     method public static String normalizeNumber(String);
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 8ea1c65..119e0ad 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -2452,6 +2452,7 @@
     method public int getFeatureType();
     method @Nullable public android.app.smartspace.SmartspaceAction getHeaderAction();
     method @NonNull public java.util.List<android.app.smartspace.SmartspaceAction> getIconGrid();
+    method @FlaggedApi("android.app.smartspace.flags.remote_views") @Nullable public android.widget.RemoteViews getRemoteViews();
     method public float getScore();
     method @Nullable public android.net.Uri getSliceUri();
     method @NonNull public String getSmartspaceTargetId();
@@ -2526,6 +2527,7 @@
     method @NonNull public android.app.smartspace.SmartspaceTarget.Builder setFeatureType(int);
     method @NonNull public android.app.smartspace.SmartspaceTarget.Builder setHeaderAction(@NonNull android.app.smartspace.SmartspaceAction);
     method @NonNull public android.app.smartspace.SmartspaceTarget.Builder setIconGrid(@NonNull java.util.List<android.app.smartspace.SmartspaceAction>);
+    method @FlaggedApi("android.app.smartspace.flags.remote_views") @NonNull public android.app.smartspace.SmartspaceTarget.Builder setRemoteViews(@NonNull android.widget.RemoteViews);
     method @NonNull public android.app.smartspace.SmartspaceTarget.Builder setScore(float);
     method @NonNull public android.app.smartspace.SmartspaceTarget.Builder setSensitive(boolean);
     method @NonNull public android.app.smartspace.SmartspaceTarget.Builder setShouldShowExpanded(boolean);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index a0b6fb7..83234a5 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -872,7 +872,7 @@
     ctor public AttributionSource(int, @Nullable String, @Nullable String);
     ctor public AttributionSource(int, @Nullable String, @Nullable String, @NonNull android.os.IBinder);
     ctor public AttributionSource(int, @Nullable String, @Nullable String, @Nullable java.util.Set<java.lang.String>, @Nullable android.content.AttributionSource);
-    ctor public AttributionSource(int, int, @Nullable String, @Nullable String, @NonNull android.os.IBinder, @Nullable String[], @Nullable android.content.AttributionSource);
+    ctor @FlaggedApi("android.permission.flags.attribution_source_constructor") public AttributionSource(int, int, @Nullable String, @Nullable String, @NonNull android.os.IBinder, @Nullable String[], @Nullable android.content.AttributionSource);
     ctor @FlaggedApi("android.permission.flags.device_aware_permission_apis") public AttributionSource(int, int, @Nullable String, @Nullable String, @NonNull android.os.IBinder, @Nullable String[], int, @Nullable android.content.AttributionSource);
     method public void enforceCallingPid();
   }
@@ -2148,7 +2148,9 @@
   }
 
   public final class BugreportParams {
+    field @FlaggedApi("android.app.admin.flags.onboarding_bugreport_v2_enabled") public static final int BUGREPORT_FLAG_KEEP_BUGREPORT_ON_RETRIEVAL = 4; // 0x4
     field @FlaggedApi("android.os.bugreport_mode_max_value") public static final int BUGREPORT_MODE_MAX_VALUE = 7; // 0x7
+    field @FlaggedApi("android.app.admin.flags.onboarding_bugreport_v2_enabled") public static final int BUGREPORT_MODE_ONBOARDING = 7; // 0x7
   }
 
   public class Build {
@@ -2287,7 +2289,7 @@
   public final class PowerManager {
     method public boolean areAutoPowerSaveModesEnabled();
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_LOW_POWER_STANDBY, android.Manifest.permission.DEVICE_POWER}) public void forceLowPowerStandbyActive(boolean);
-    method public boolean isBatterySaverSupported();
+    method @FlaggedApi("android.os.battery_saver_supported_check_api") public boolean isBatterySaverSupported();
     field public static final String ACTION_ENHANCED_DISCHARGE_PREDICTION_CHANGED = "android.os.action.ENHANCED_DISCHARGE_PREDICTION_CHANGED";
     field @RequiresPermission(android.Manifest.permission.DEVICE_POWER) public static final int SYSTEM_WAKELOCK = -2147483648; // 0x80000000
   }
@@ -3491,10 +3493,6 @@
     method public boolean isSystemGroup();
   }
 
-  public abstract class LayoutInflater {
-    method public void setPrecompiledLayoutsEnabledForTesting(boolean);
-  }
-
   public final class MotionEvent extends android.view.InputEvent implements android.os.Parcelable {
     method public int getDisplayId();
     method public void setActionButton(int);
diff --git a/core/java/android/app/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java
index 37111e9..c8317c8 100644
--- a/core/java/android/app/ApplicationStartInfo.java
+++ b/core/java/android/app/ApplicationStartInfo.java
@@ -27,7 +27,15 @@
 import android.os.Parcelable;
 import android.os.UserHandle;
 import android.util.ArrayMap;
+import android.util.proto.ProtoInputStream;
+import android.util.proto.ProtoOutputStream;
+import android.util.proto.WireTypeMismatchException;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -608,6 +616,103 @@
                 }
             };
 
+    /**
+     * Write to a protocol buffer output stream. Protocol buffer message definition at {@link
+     * android.app.ApplicationStartInfoProto}
+     *
+     * @param proto Stream to write the ApplicationStartInfo object to.
+     * @param fieldId Field Id of the ApplicationStartInfo as defined in the parent message
+     * @hide
+     */
+    public void writeToProto(ProtoOutputStream proto, long fieldId) throws IOException {
+        final long token = proto.start(fieldId);
+        proto.write(ApplicationStartInfoProto.PID, mPid);
+        proto.write(ApplicationStartInfoProto.REAL_UID, mRealUid);
+        proto.write(ApplicationStartInfoProto.PACKAGE_UID, mPackageUid);
+        proto.write(ApplicationStartInfoProto.DEFINING_UID, mDefiningUid);
+        proto.write(ApplicationStartInfoProto.PROCESS_NAME, mProcessName);
+        proto.write(ApplicationStartInfoProto.STARTUP_STATE, mStartupState);
+        proto.write(ApplicationStartInfoProto.REASON, mReason);
+        if (mStartupTimestampsNs != null && mStartupTimestampsNs.size() > 0) {
+            ByteArrayOutputStream timestampsBytes = new ByteArrayOutputStream();
+            ObjectOutputStream timestampsOut = new ObjectOutputStream(timestampsBytes);
+            timestampsOut.writeObject(mStartupTimestampsNs);
+            proto.write(ApplicationStartInfoProto.STARTUP_TIMESTAMPS,
+                    timestampsBytes.toByteArray());
+        }
+        proto.write(ApplicationStartInfoProto.START_TYPE, mStartType);
+        if (mStartIntent != null) {
+            Parcel parcel = Parcel.obtain();
+            mStartIntent.writeToParcel(parcel, 0);
+            proto.write(ApplicationStartInfoProto.START_INTENT, parcel.marshall());
+            parcel.recycle();
+        }
+        proto.write(ApplicationStartInfoProto.LAUNCH_MODE, mLaunchMode);
+        proto.end(token);
+    }
+
+    /**
+     * Read from a protocol buffer input stream. Protocol buffer message definition at {@link
+     * android.app.ApplicationStartInfoProto}
+     *
+     * @param proto Stream to read the ApplicationStartInfo object from.
+     * @param fieldId Field Id of the ApplicationStartInfo as defined in the parent message
+     * @hide
+     */
+    public void readFromProto(ProtoInputStream proto, long fieldId)
+            throws IOException, WireTypeMismatchException, ClassNotFoundException {
+        final long token = proto.start(fieldId);
+        while (proto.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+            switch (proto.getFieldNumber()) {
+                case (int) ApplicationStartInfoProto.PID:
+                    mPid = proto.readInt(ApplicationStartInfoProto.PID);
+                    break;
+                case (int) ApplicationStartInfoProto.REAL_UID:
+                    mRealUid = proto.readInt(ApplicationStartInfoProto.REAL_UID);
+                    break;
+                case (int) ApplicationStartInfoProto.PACKAGE_UID:
+                    mPackageUid = proto.readInt(ApplicationStartInfoProto.PACKAGE_UID);
+                    break;
+                case (int) ApplicationStartInfoProto.DEFINING_UID:
+                    mDefiningUid = proto.readInt(ApplicationStartInfoProto.DEFINING_UID);
+                    break;
+                case (int) ApplicationStartInfoProto.PROCESS_NAME:
+                    mProcessName = intern(proto.readString(ApplicationStartInfoProto.PROCESS_NAME));
+                    break;
+                case (int) ApplicationStartInfoProto.STARTUP_STATE:
+                    mStartupState = proto.readInt(ApplicationStartInfoProto.STARTUP_STATE);
+                    break;
+                case (int) ApplicationStartInfoProto.REASON:
+                    mReason = proto.readInt(ApplicationStartInfoProto.REASON);
+                    break;
+                case (int) ApplicationStartInfoProto.STARTUP_TIMESTAMPS:
+                    ByteArrayInputStream timestampsBytes = new ByteArrayInputStream(proto.readBytes(
+                            ApplicationStartInfoProto.STARTUP_TIMESTAMPS));
+                    ObjectInputStream timestampsIn = new ObjectInputStream(timestampsBytes);
+                    mStartupTimestampsNs = (ArrayMap<Integer, Long>) timestampsIn.readObject();
+                    break;
+                case (int) ApplicationStartInfoProto.START_TYPE:
+                    mStartType = proto.readInt(ApplicationStartInfoProto.START_TYPE);
+                    break;
+                case (int) ApplicationStartInfoProto.START_INTENT:
+                    byte[] startIntentBytes = proto.readBytes(
+                        ApplicationStartInfoProto.START_INTENT);
+                    if (startIntentBytes.length > 0) {
+                        Parcel parcel = Parcel.obtain();
+                        parcel.unmarshall(startIntentBytes, 0, startIntentBytes.length);
+                        parcel.setDataPosition(0);
+                        mStartIntent = Intent.CREATOR.createFromParcel(parcel);
+                        parcel.recycle();
+                    }
+                    break;
+                case (int) ApplicationStartInfoProto.LAUNCH_MODE:
+                    mLaunchMode = proto.readInt(ApplicationStartInfoProto.LAUNCH_MODE);
+                    break;
+            }
+        }
+        proto.end(token);
+    }
+
     /** @hide */
     public void dump(@NonNull PrintWriter pw, @Nullable String prefix, @Nullable String seqSuffix,
             @NonNull SimpleDateFormat sdf) {
diff --git a/core/java/android/app/EnterTransitionCoordinator.java b/core/java/android/app/EnterTransitionCoordinator.java
index e2082f7..180725e 100644
--- a/core/java/android/app/EnterTransitionCoordinator.java
+++ b/core/java/android/app/EnterTransitionCoordinator.java
@@ -706,8 +706,12 @@
     }
 
     private boolean allowOverlappingTransitions() {
-        return mIsReturning ? getWindow().getAllowReturnTransitionOverlap()
-                : getWindow().getAllowEnterTransitionOverlap();
+        final Window window = getWindow();
+        if (window == null) {
+            return false;
+        }
+        return mIsReturning ? window.getAllowReturnTransitionOverlap()
+                : window.getAllowEnterTransitionOverlap();
     }
 
     private void startRejectedAnimations(final ArrayList<View> rejectedSnapshots) {
diff --git a/core/java/android/app/servertransaction/ClientTransactionListenerController.java b/core/java/android/app/servertransaction/ClientTransactionListenerController.java
index e2aee83..7418c06 100644
--- a/core/java/android/app/servertransaction/ClientTransactionListenerController.java
+++ b/core/java/android/app/servertransaction/ClientTransactionListenerController.java
@@ -20,46 +20,31 @@
 
 import static java.util.Objects.requireNonNull;
 
-import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
+import android.app.ActivityThread;
+import android.hardware.display.DisplayManagerGlobal;
 import android.os.Process;
-import android.util.ArrayMap;
 
-import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 
-import java.util.ArrayList;
-import java.util.concurrent.Executor;
-import java.util.function.IntConsumer;
-
 /**
  * Singleton controller to manage listeners to individual {@link ClientTransaction}.
  *
- * TODO(b/260873529) make as TestApi to allow CTS.
  * @hide
  */
 public class ClientTransactionListenerController {
 
     private static ClientTransactionListenerController sController;
 
-    private final Object mLock = new Object();
-
-    /**
-     * Mapping from client registered listener for display change to the corresponding
-     * {@link Executor} to invoke the listener on.
-     * @see #registerDisplayChangeListener(IntConsumer, Executor)
-     */
-    @GuardedBy("mLock")
-    private final ArrayMap<IntConsumer, Executor> mDisplayChangeListeners = new ArrayMap<>();
-
-    private final ArrayList<IntConsumer> mTmpDisplayChangeListeners = new ArrayList<>();
+    private final DisplayManagerGlobal mDisplayManager;
 
     /** Gets the singleton controller. */
     @NonNull
     public static ClientTransactionListenerController getInstance() {
         synchronized (ClientTransactionListenerController.class) {
             if (sController == null) {
-                sController = new ClientTransactionListenerController();
+                sController = new ClientTransactionListenerController(
+                        DisplayManagerGlobal.getInstance());
             }
             return sController;
         }
@@ -68,45 +53,13 @@
     /** Creates a new instance for test only. */
     @VisibleForTesting
     @NonNull
-    public static ClientTransactionListenerController createInstanceForTesting() {
-        return new ClientTransactionListenerController();
+    public static ClientTransactionListenerController createInstanceForTesting(
+            @NonNull DisplayManagerGlobal displayManager) {
+        return new ClientTransactionListenerController(displayManager);
     }
 
-    private ClientTransactionListenerController() {}
-
-    /**
-     * Registers a new listener for display change. It will be invoked when receives a
-     * {@link ClientTransaction} that is updating display-related window configuration, such as
-     * bounds and rotation.
-     *
-     * WHen triggered, the listener will be invoked with the logical display id that was changed.
-     *
-     * @param listener the listener to invoke when receives a transaction with Display change.
-     * @param executor the executor on which callback method will be invoked.
-     */
-    public void registerDisplayChangeListener(@NonNull IntConsumer listener,
-            @NonNull @CallbackExecutor Executor executor) {
-        if (!isSyncWindowConfigUpdateFlagEnabled()) {
-            return;
-        }
-        requireNonNull(listener);
-        requireNonNull(executor);
-        synchronized (mLock) {
-            mDisplayChangeListeners.put(listener, executor);
-        }
-    }
-
-    /**
-     * Unregisters the listener for display change that was previously registered through
-     * {@link #registerDisplayChangeListener}.
-     */
-    public void unregisterDisplayChangeListener(@NonNull IntConsumer listener) {
-        if (!isSyncWindowConfigUpdateFlagEnabled()) {
-            return;
-        }
-        synchronized (mLock) {
-            mDisplayChangeListeners.remove(listener);
-        }
+    private ClientTransactionListenerController(@NonNull DisplayManagerGlobal displayManager) {
+        mDisplayManager = requireNonNull(displayManager);
     }
 
     /**
@@ -117,24 +70,14 @@
         if (!isSyncWindowConfigUpdateFlagEnabled()) {
             return;
         }
-        synchronized (mLock) {
-            // Make a copy of the list to avoid listener removal during callback.
-            mTmpDisplayChangeListeners.addAll(mDisplayChangeListeners.keySet());
-            final int num = mTmpDisplayChangeListeners.size();
-            try {
-                for (int i = 0; i < num; i++) {
-                    final IntConsumer listener = mTmpDisplayChangeListeners.get(i);
-                    final Executor executor = mDisplayChangeListeners.get(listener);
-                    executor.execute(() -> listener.accept(displayId));
-                }
-            } finally {
-                mTmpDisplayChangeListeners.clear();
-            }
+        if (ActivityThread.isSystem()) {
+            // Not enable for system server.
+            return;
         }
+        mDisplayManager.handleDisplayChangeFromWindowManager(displayId);
     }
 
     /** Whether {@link #syncWindowConfigUpdateFlag} feature flag is enabled. */
-    @VisibleForTesting
     public boolean isSyncWindowConfigUpdateFlagEnabled() {
         // Can't read flag from isolated process.
         return !Process.isIsolated() && syncWindowConfigUpdateFlag();
diff --git a/core/java/android/app/smartspace/SmartspaceTarget.java b/core/java/android/app/smartspace/SmartspaceTarget.java
index 3c66a15..f6f65c7 100644
--- a/core/java/android/app/smartspace/SmartspaceTarget.java
+++ b/core/java/android/app/smartspace/SmartspaceTarget.java
@@ -16,10 +16,12 @@
 package android.app.smartspace;
 
 import android.annotation.CurrentTimeMillisLong;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
+import android.app.smartspace.flags.Flags;
 import android.app.smartspace.uitemplatedata.BaseTemplateData;
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.ComponentName;
@@ -27,6 +29,7 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.UserHandle;
+import android.widget.RemoteViews;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -41,7 +44,8 @@
  * {@link SmartspaceAction} as their type because they can have associated actions.
  *
  * <p><b>NOTE: </b>
- * If {@link mWidget} is set, it should be preferred over all other properties.
+ * If either {@link mRemoteViews} or {@link mWidget} is set, it should be preferred over all
+ * other properties. (An exception is thrown if both are set.)
  * Else, if {@link mSliceUri} is set, it should be preferred over all other data properties.
  * Otherwise, the instance should be treated as a data object.
  *
@@ -133,6 +137,9 @@
     private final AppWidgetProviderInfo mWidget;
 
     @Nullable
+    private final RemoteViews mRemoteViews;
+
+    @Nullable
     private final BaseTemplateData mTemplateData;
 
     public static final int FEATURE_UNDEFINED = 0;
@@ -288,6 +295,7 @@
         this.mSliceUri = in.readTypedObject(Uri.CREATOR);
         this.mWidget = in.readTypedObject(AppWidgetProviderInfo.CREATOR);
         this.mTemplateData = in.readParcelable(/* loader= */null, BaseTemplateData.class);
+        this.mRemoteViews = in.readTypedObject(RemoteViews.CREATOR);
     }
 
     private SmartspaceTarget(String smartspaceTargetId,
@@ -298,7 +306,7 @@
             boolean shouldShowExpanded, String sourceNotificationKey,
             ComponentName componentName, UserHandle userHandle,
             String associatedSmartspaceTargetId, Uri sliceUri,
-            AppWidgetProviderInfo widget, BaseTemplateData templateData) {
+            AppWidgetProviderInfo widget, BaseTemplateData templateData, RemoteViews remoteViews) {
         mSmartspaceTargetId = smartspaceTargetId;
         mHeaderAction = headerAction;
         mBaseAction = baseAction;
@@ -317,6 +325,7 @@
         mSliceUri = sliceUri;
         mWidget = widget;
         mTemplateData = templateData;
+        mRemoteViews = remoteViews;
     }
 
     /**
@@ -461,6 +470,15 @@
     }
 
     /**
+     * Returns the {@link RemoteViews} to show over the target.
+     */
+    @FlaggedApi(Flags.FLAG_REMOTE_VIEWS)
+    @Nullable
+    public RemoteViews getRemoteViews() {
+        return mRemoteViews;
+    }
+
+    /**
      * @see Parcelable.Creator
      */
     @NonNull
@@ -496,6 +514,7 @@
         dest.writeTypedObject(this.mSliceUri, flags);
         dest.writeTypedObject(this.mWidget, flags);
         dest.writeParcelable(this.mTemplateData, flags);
+        dest.writeTypedObject(this.mRemoteViews, flags);
     }
 
     @Override
@@ -524,6 +543,7 @@
                 + ", mSliceUri=" + mSliceUri
                 + ", mWidget=" + mWidget
                 + ", mTemplateData=" + mTemplateData
+                + ", mRemoteViews=" + mRemoteViews
                 + '}';
     }
 
@@ -550,7 +570,8 @@
                 that.mAssociatedSmartspaceTargetId)
                 && Objects.equals(mSliceUri, that.mSliceUri)
                 && Objects.equals(mWidget, that.mWidget)
-                && Objects.equals(mTemplateData, that.mTemplateData);
+                && Objects.equals(mTemplateData, that.mTemplateData)
+                && Objects.equals(mRemoteViews, that.mRemoteViews);
     }
 
     @Override
@@ -558,7 +579,7 @@
         return Objects.hash(mSmartspaceTargetId, mHeaderAction, mBaseAction, mCreationTimeMillis,
                 mExpiryTimeMillis, mScore, mActionChips, mIconGrid, mFeatureType, mSensitive,
                 mShouldShowExpanded, mSourceNotificationKey, mComponentName, mUserHandle,
-                mAssociatedSmartspaceTargetId, mSliceUri, mWidget, mTemplateData);
+                mAssociatedSmartspaceTargetId, mSliceUri, mWidget, mTemplateData, mRemoteViews);
     }
 
     /**
@@ -588,6 +609,8 @@
         private AppWidgetProviderInfo mWidget;
         private BaseTemplateData mTemplateData;
 
+        private RemoteViews mRemoteViews;
+
         /**
          * A builder for {@link SmartspaceTarget}.
          *
@@ -727,9 +750,15 @@
          *
          * <p><b>NOTE: </b> If {@link mWidget} is set, all other @Nullable params should be
          * ignored.
+         *
+         * @throws An {@link IllegalStateException} is thrown if {@link mRemoteViews} is set.
          */
         @NonNull
         public Builder setWidget(@NonNull AppWidgetProviderInfo widget) {
+            if (mRemoteViews != null) {
+                throw new IllegalStateException(
+                        "Widget providers and RemoteViews cannot be used at the same time.");
+            }
             this.mWidget = widget;
             return this;
         }
@@ -745,6 +774,25 @@
         }
 
         /**
+         * Sets the {@link RemoteViews}.
+         *
+         * <p><b>NOTE: </b> If {@link RemoteViews} is set, all other @Nullable params should be
+         * ignored.
+         *
+         * @throws An {@link IllegalStateException} is thrown if {@link mWidget} is set.
+         */
+        @FlaggedApi(Flags.FLAG_REMOTE_VIEWS)
+        @NonNull
+        public Builder setRemoteViews(@NonNull RemoteViews remoteViews) {
+            if (mWidget != null) {
+                throw new IllegalStateException(
+                        "Widget providers and RemoteViews cannot be used at the same time.");
+            }
+            mRemoteViews = remoteViews;
+            return this;
+        }
+
+        /**
          * Builds a new {@link SmartspaceTarget}.
          *
          * @throws IllegalStateException when non null fields are set as null.
@@ -760,7 +808,7 @@
                     mHeaderAction, mBaseAction, mCreationTimeMillis, mExpiryTimeMillis, mScore,
                     mActionChips, mIconGrid, mFeatureType, mSensitive, mShouldShowExpanded,
                     mSourceNotificationKey, mComponentName, mUserHandle,
-                    mAssociatedSmartspaceTargetId, mSliceUri, mWidget, mTemplateData);
+                    mAssociatedSmartspaceTargetId, mSliceUri, mWidget, mTemplateData, mRemoteViews);
         }
     }
 }
diff --git a/core/java/android/app/smartspace/flags.aconfig b/core/java/android/app/smartspace/flags.aconfig
new file mode 100644
index 0000000..12af888
--- /dev/null
+++ b/core/java/android/app/smartspace/flags.aconfig
@@ -0,0 +1,15 @@
+package: "android.app.smartspace.flags"
+
+flag {
+  name: "remote_views"
+  namespace: "sysui_integrations"
+  description: "Flag to enable the FlaggedApi to include RemoteViews in SmartspaceTarget"
+  bug: "300157758"
+}
+
+flag {
+  name: "access_smartspace"
+  namespace: "sysui_integrations"
+  description: "Flag to enable the ACCESS_SMARTSPACE check in SmartspaceManagerService"
+  bug: "297207196"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileLifecycle.kt b/core/java/android/app/usage/ParcelableUsageEventList.aidl
similarity index 78%
rename from packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileLifecycle.kt
rename to core/java/android/app/usage/ParcelableUsageEventList.aidl
index 6d7c576..1652996 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileLifecycle.kt
+++ b/core/java/android/app/usage/ParcelableUsageEventList.aidl
@@ -5,7 +5,7 @@
  * 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
+ *     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,
@@ -14,9 +14,6 @@
  * limitations under the License.
  */
 
-package com.android.systemui.qs.tiles.viewmodel
+package android.app.usage;
 
-enum class QSTileLifecycle {
-    ALIVE,
-    DEAD,
-}
+parcelable ParcelableUsageEventList;
diff --git a/core/java/android/app/usage/ParcelableUsageEventList.java b/core/java/android/app/usage/ParcelableUsageEventList.java
new file mode 100644
index 0000000..016d97f
--- /dev/null
+++ b/core/java/android/app/usage/ParcelableUsageEventList.java
@@ -0,0 +1,266 @@
+/*
+ * 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.app.usage;
+
+import android.annotation.NonNull;
+import android.app.usage.UsageEvents.Event;
+import android.content.res.Configuration;
+import android.os.BadParcelableException;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This is a copied version of BaseParceledListSlice with specific
+ * {@link UsageEvents.Event} instance that used to transfer the large
+ * list of {@link UsageEvents.Event} objects across an IPC. Splits
+ * into multiple transactions if needed.
+ *
+ * @see BasedParceledListSlice
+ *
+ * @hide
+ */
+public final class ParcelableUsageEventList implements Parcelable {
+    private static final String TAG = "ParcelableUsageEventList";
+    private static final boolean DEBUG = false;
+    private static final boolean DEBUG_ALL = false;
+
+    private static final int MAX_IPC_SIZE = IBinder.getSuggestedMaxIpcSizeBytes();
+
+    private List<Event> mList;
+
+    public ParcelableUsageEventList(List<Event> list) {
+        mList = list;
+    }
+
+    private ParcelableUsageEventList(Parcel in) {
+        final int N = in.readInt();
+        mList = new ArrayList<>();
+        if (DEBUG) Log.d(TAG, "Retrieving " + N + " items");
+        if (N <= 0) {
+            return;
+        }
+
+        int i = 0;
+        while (i < N) {
+            if (in.readInt() == 0) {
+                break;
+            }
+            mList.add(readEventFromParcel(in));
+            if (DEBUG_ALL) Log.d(TAG, "Read inline #" + i + ": " + mList.get(mList.size() - 1));
+            i++;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "Read " + mList.size() + " inline UsageEvents"
+                    + ", total N=" + N + " UsageEvents");
+        }
+        if (i >= N) {
+            return;
+        }
+        final IBinder retriever = in.readStrongBinder();
+        while (i < N) {
+            if (DEBUG) Log.d(TAG, "Reading more @" + i + " of " + N + ": retriever=" + retriever);
+            Parcel data = Parcel.obtain();
+            Parcel reply = Parcel.obtain();
+            data.writeInt(i);
+            try {
+                retriever.transact(IBinder.FIRST_CALL_TRANSACTION, data, reply, 0);
+                reply.readException();
+                int count = 0;
+                while (i < N && reply.readInt() != 0) {
+                    mList.add(readEventFromParcel(reply));
+                    if (DEBUG_ALL) {
+                        Log.d(TAG, "Read extra #" + i + ": " + mList.get(mList.size() - 1));
+                    }
+                    i++;
+                    count++;
+                }
+                if (DEBUG) Log.d(TAG, "Read extra @" + count + " of " + N);
+            } catch (RemoteException e) {
+                throw new BadParcelableException(
+                    "Failure retrieving array; only received " + i + " of " + N, e);
+            } finally {
+                reply.recycle();
+                data.recycle();
+            }
+        }
+        if (DEBUG) Log.d(TAG, "Finish reading total " + i + " UsageEvents");
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        final int N = mList.size();
+        final int callFlags = flags;
+        dest.writeInt(N);
+        if (DEBUG) Log.d(TAG, "Writing " + N + " items");
+        if (N > 0) {
+            int i = 0;
+            while (i < N && dest.dataSize() < MAX_IPC_SIZE) {
+                dest.writeInt(1);
+
+                final Event event = mList.get(i);
+                writeEventToParcel(event, dest, callFlags);
+
+                if (DEBUG_ALL) Log.d(TAG, "Wrote inline #" + i + ": " + mList.get(i));
+                i++;
+            }
+            if (i < N) {
+                dest.writeInt(0);
+                Binder retriever = new Binder() {
+                    @Override
+                    protected boolean onTransact(int code, Parcel data, Parcel reply, int flags)
+                            throws RemoteException {
+                        if (code != FIRST_CALL_TRANSACTION) {
+                            return super.onTransact(code, data, reply, flags);
+                        } else if (mList == null) {
+                            throw new IllegalArgumentException("Attempt to transfer null list, "
+                                + "did transfer finish?");
+                        }
+                        int i = data.readInt();
+
+                        if (DEBUG) {
+                            Log.d(TAG, "Writing more @" + i + " of " + N + " to "
+                                    + Binder.getCallingPid() + ", sender=" + this);
+                        }
+
+                        try {
+                            reply.writeNoException();
+                            int count = 0;
+                            while (i < N && reply.dataSize() < MAX_IPC_SIZE) {
+                                reply.writeInt(1);
+
+                                final Event event = mList.get(i);
+                                writeEventToParcel(event, reply, callFlags);
+
+                                if (DEBUG_ALL) {
+                                    Log.d(TAG, "Wrote extra #" + i + ": " + mList.get(i));
+                                }
+                                i++;
+                                count++;
+                            }
+                            if (i < N) {
+                                if (DEBUG) {
+                                    Log.d(TAG, "Breaking @" + i + " of " + N
+                                            + "(count = " + count + ")");
+                                }
+                                reply.writeInt(0);
+                            } else {
+                                if (DEBUG) Log.d(TAG, "Transfer done, clearing mList reference");
+                                mList = null;
+                            }
+                        } catch (RuntimeException e) {
+                            if (DEBUG) Log.d(TAG, "Transfer failed, clearing mList reference");
+                            mList = null;
+                            throw e;
+                        }
+                        return true;
+                    }
+                };
+                if (DEBUG) Log.d(TAG, "Breaking @" + i + " of " + N + ": retriever=" + retriever);
+                dest.writeStrongBinder(retriever);
+            }
+        }
+    }
+
+    public List<Event> getList() {
+        return mList;
+    }
+
+    public static final Parcelable.Creator<ParcelableUsageEventList> CREATOR =
+            new Parcelable.Creator<ParcelableUsageEventList>() {
+                public ParcelableUsageEventList createFromParcel(Parcel in) {
+                    return new ParcelableUsageEventList(in);
+                }
+
+                @Override
+                public ParcelableUsageEventList[] newArray(int size) {
+                    return new ParcelableUsageEventList[size];
+                }
+            };
+
+    private Event readEventFromParcel(Parcel in) {
+        final Event event = new Event();
+        event.mPackage = in.readString();
+        event.mClass = in.readString();
+        event.mInstanceId = in.readInt();
+        event.mTaskRootPackage = in.readString();
+        event.mTaskRootClass = in.readString();
+        event.mEventType = in.readInt();
+        event.mTimeStamp = in.readLong();
+
+        // Fill out the event-dependant fields.
+        event.mConfiguration = null;
+        event.mShortcutId = null;
+        event.mAction = null;
+        event.mContentType = null;
+        event.mContentAnnotations = null;
+        event.mNotificationChannelId = null;
+        event.mLocusId = null;
+
+        switch (event.mEventType) {
+            case Event.CONFIGURATION_CHANGE -> {
+                event.mConfiguration = Configuration.CREATOR.createFromParcel(in);
+            }
+            case Event.SHORTCUT_INVOCATION -> event.mShortcutId = in.readString();
+            case Event.CHOOSER_ACTION -> {
+                event.mAction = in.readString();
+                event.mContentType = in.readString();
+                event.mContentAnnotations = in.readStringArray();
+            }
+            case Event.STANDBY_BUCKET_CHANGED -> event.mBucketAndReason = in.readInt();
+            case Event.NOTIFICATION_INTERRUPTION -> event.mNotificationChannelId = in.readString();
+            case Event.LOCUS_ID_SET -> event.mLocusId = in.readString();
+        }
+        event.mFlags = in.readInt();
+
+        return event;
+    }
+
+    private void writeEventToParcel(@NonNull Event event, @NonNull Parcel dest, int flags) {
+        dest.writeString(event.mPackage);
+        dest.writeString(event.mClass);
+        dest.writeInt(event.mInstanceId);
+        dest.writeString(event.mTaskRootPackage);
+        dest.writeString(event.mTaskRootClass);
+        dest.writeInt(event.mEventType);
+        dest.writeLong(event.mTimeStamp);
+
+        switch (event.mEventType) {
+            case Event.CONFIGURATION_CHANGE -> event.mConfiguration.writeToParcel(dest, flags);
+            case Event.SHORTCUT_INVOCATION -> dest.writeString(event.mShortcutId);
+            case Event.CHOOSER_ACTION -> {
+                dest.writeString(event.mAction);
+                dest.writeString(event.mContentType);
+                dest.writeStringArray(event.mContentAnnotations);
+            }
+            case Event.STANDBY_BUCKET_CHANGED -> dest.writeInt(event.mBucketAndReason);
+            case Event.NOTIFICATION_INTERRUPTION -> dest.writeString(event.mNotificationChannelId);
+            case Event.LOCUS_ID_SET -> dest.writeString(event.mLocusId);
+        }
+        dest.writeInt(event.mFlags);
+    }
+}
diff --git a/core/java/android/app/usage/UsageEvents.java b/core/java/android/app/usage/UsageEvents.java
index 3c256ad..c188686 100644
--- a/core/java/android/app/usage/UsageEvents.java
+++ b/core/java/android/app/usage/UsageEvents.java
@@ -714,7 +714,7 @@
     @UnsupportedAppUsage
     private Parcel mParcel = null;
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
-    private final int mEventCount;
+    private int mEventCount;
 
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     private int mIndex = 0;
@@ -735,6 +735,23 @@
      */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     public UsageEvents(Parcel in) {
+        if (Flags.useParceledList()) {
+            readUsageEventsFromParcelWithParceledList(in);
+        } else {
+            readUsageEventsFromParcelWithBlob(in);
+        }
+
+        mIncludeTaskRoots = true;
+    }
+
+    private void readUsageEventsFromParcelWithParceledList(Parcel in) {
+        mIndex = in.readInt();
+        mEventsToWrite = in.readParcelable(UsageEvents.class.getClassLoader(),
+            ParcelableUsageEventList.class).getList();
+        mEventCount = mEventsToWrite.size();
+    }
+
+    private void readUsageEventsFromParcelWithBlob(Parcel in) {
         byte[] bytes = in.readBlob();
         Parcel data = Parcel.obtain();
         data.unmarshall(bytes, 0, bytes.length);
@@ -752,7 +769,6 @@
             mParcel.setDataSize(mParcel.dataPosition());
             mParcel.setDataPosition(positionInParcel);
         }
-        mIncludeTaskRoots = true;
     }
 
     /**
@@ -810,6 +826,10 @@
             return false;
         }
 
+        if (Flags.useParceledList()) {
+            return getNextEventFromParceledList(eventOut);
+        }
+
         if (mParcel != null) {
             readEventFromParcel(mParcel, eventOut);
         } else {
@@ -824,6 +844,12 @@
         return true;
     }
 
+    private boolean getNextEventFromParceledList(Event eventOut) {
+        eventOut.copyFrom(mEventsToWrite.get(mIndex));
+        mIndex++;
+        return true;
+    }
+
     /**
      * Resets the collection so that it can be iterated over from the beginning.
      *
@@ -968,7 +994,7 @@
             case Event.CHOOSER_ACTION:
                 eventOut.mAction = p.readString();
                 eventOut.mContentType = p.readString();
-                eventOut.mContentAnnotations = p.createStringArray();
+                eventOut.mContentAnnotations = p.readStringArray();
                 break;
             case Event.STANDBY_BUCKET_CHANGED:
                 eventOut.mBucketAndReason = p.readInt();
@@ -990,6 +1016,19 @@
 
     @Override
     public void writeToParcel(Parcel dest, int flags) {
+        if (Flags.useParceledList()) {
+            writeUsageEventsToParcelWithParceledList(dest, flags);
+        } else {
+            writeUsageEventsToParcelWithBlob(dest, flags);
+        }
+    }
+
+    private void writeUsageEventsToParcelWithParceledList(Parcel dest, int flags) {
+        dest.writeInt(mIndex);
+        dest.writeParcelable(new ParcelableUsageEventList(mEventsToWrite), flags);
+    }
+
+    private void writeUsageEventsToParcelWithBlob(Parcel dest, int flags) {
         Parcel data = Parcel.obtain();
         data.writeInt(mEventCount);
         data.writeInt(mIndex);
diff --git a/core/java/android/app/usage/flags.aconfig b/core/java/android/app/usage/flags.aconfig
index 4f1c65b..0b8e29f 100644
--- a/core/java/android/app/usage/flags.aconfig
+++ b/core/java/android/app/usage/flags.aconfig
@@ -21,3 +21,10 @@
     is_fixed_read_only: true
     bug: "299336442"
 }
+
+flag {
+    name: "use_parceled_list"
+    namespace: "backstage_power"
+    description: "Flag for parcelable usage event list"
+    bug: "301254110"
+}
diff --git a/core/java/android/content/AttributionSource.java b/core/java/android/content/AttributionSource.java
index 62fbcaf..4b2cee6 100644
--- a/core/java/android/content/AttributionSource.java
+++ b/core/java/android/content/AttributionSource.java
@@ -155,6 +155,7 @@
 
     /** @hide */
     @TestApi
+    @FlaggedApi(Flags.FLAG_ATTRIBUTION_SOURCE_CONSTRUCTOR)
     public AttributionSource(int uid, int pid, @Nullable String packageName,
             @Nullable String attributionTag, @NonNull IBinder token,
             @Nullable String[] renouncedPermissions,
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 75370d9..1c917ee 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -861,12 +861,23 @@
      * <em>must</em> be sure to use {@link #unregisterComponentCallbacks} when
      * appropriate in the future; this will not be removed for you.
      * <p>
-     * After {@link Build.VERSION_CODES#S}, Registering the ComponentCallbacks to Context created
+     * After {@link Build.VERSION_CODES#S}, registering the ComponentCallbacks to Context created
      * via {@link #createWindowContext(int, Bundle)} or
      * {@link #createWindowContext(Display, int, Bundle)} will receive
      * {@link ComponentCallbacks#onConfigurationChanged(Configuration)} from Window Context rather
      * than its base application. It is helpful if you want to handle UI components that
      * associated with the Window Context when the Window Context has configuration changes.</p>
+     * <p>
+     * After {@link Build.VERSION_CODES#TIRAMISU}, registering the ComponentCallbacks to
+     * {@link Activity} context will receive
+     * {@link ComponentCallbacks#onConfigurationChanged(Configuration)} from
+     * {@link Activity#onConfigurationChanged(Configuration)} rather than its base application.</p>
+     * <p>
+     * After {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, registering the ComponentCallbacks to
+     * {@link android.inputmethodservice.InputMethodService} will receive
+     * {@link ComponentCallbacks#onConfigurationChanged(Configuration)} from InputmethodService
+     * rather than its base application. It is helpful if you want to handle UI components when the
+     * IME has configuration changes.</p>
      *
      * @param callback The interface to call.  This can be either a
      * {@link ComponentCallbacks} or {@link ComponentCallbacks2} interface.
diff --git a/core/java/android/content/pm/SigningInfo.java b/core/java/android/content/pm/SigningInfo.java
index 554de0c..543703e 100644
--- a/core/java/android/content/pm/SigningInfo.java
+++ b/core/java/android/content/pm/SigningInfo.java
@@ -16,9 +16,16 @@
 
 package android.content.pm;
 
+import android.annotation.FlaggedApi;
+import android.annotation.IntRange;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.util.ArraySet;
+
+import java.security.PublicKey;
+import java.util.Collection;
 
 /**
  * Information pertaining to the signing certificates used to sign a package.
@@ -33,6 +40,43 @@
     }
 
     /**
+     * Creates a new instance of information used to sign the APK.
+     *
+     * @param schemeVersion version of signing schema.
+     * @param apkContentsSigners signing certificates.
+     * @param publicKeys for the signing certificates.
+     * @param signingCertificateHistory All signing certificates the package has proven it is
+     *                                  authorized to use.
+     *
+     * @see
+     * <a href="https://source.android.com/docs/security/features/apksigning#schemes">APK signing
+     * schemas</a>
+     */
+    @FlaggedApi(Flags.FLAG_ARCHIVING)
+    public SigningInfo(@IntRange(from = 0) int schemeVersion,
+            @Nullable Collection<Signature> apkContentsSigners,
+            @Nullable Collection<PublicKey> publicKeys,
+            @Nullable Collection<Signature> signingCertificateHistory) {
+        if (schemeVersion <= 0 || apkContentsSigners == null) {
+            mSigningDetails = SigningDetails.UNKNOWN;
+            return;
+        }
+        Signature[] signatures = apkContentsSigners != null && !apkContentsSigners.isEmpty()
+                ? apkContentsSigners.toArray(new Signature[apkContentsSigners.size()])
+                : null;
+        Signature[] pastSignatures =
+                signingCertificateHistory != null && !signingCertificateHistory.isEmpty()
+                ? signingCertificateHistory.toArray(new Signature[signingCertificateHistory.size()])
+                : null;
+        if (Signature.areExactArraysMatch(signatures, pastSignatures)) {
+            pastSignatures = null;
+        }
+        ArraySet<PublicKey> keys =
+                publicKeys != null && !publicKeys.isEmpty() ? new ArraySet<>(publicKeys) : null;
+        mSigningDetails = new SigningDetails(signatures, schemeVersion, keys, pastSignatures);
+    }
+
+    /**
      * @hide only packagemanager should be populating this
      */
     public SigningInfo(SigningDetails signingDetails) {
@@ -116,6 +160,26 @@
         return mSigningDetails.getSignatures();
     }
 
+    /**
+     * Returns the version of signing schema used to sign the APK.
+     *
+     * @see
+     * <a href="https://source.android.com/docs/security/features/apksigning#schemes">APK signing
+     * schemas</a>
+     */
+    @FlaggedApi(Flags.FLAG_ARCHIVING)
+    public @IntRange(from = 0) int getSchemeVersion() {
+        return mSigningDetails.getSignatureSchemeVersion();
+    }
+
+    /**
+     * Returns the public keys for the signing certificates.
+     */
+    @FlaggedApi(Flags.FLAG_ARCHIVING)
+    public @NonNull Collection<PublicKey> getPublicKeys() {
+        return mSigningDetails.getPublicKeys();
+    }
+
     @Override
     public int describeContents() {
         return 0;
diff --git a/core/java/android/hardware/biometrics/BiometricManager.java b/core/java/android/hardware/biometrics/BiometricManager.java
index 9c05dfc..82694ee 100644
--- a/core/java/android/hardware/biometrics/BiometricManager.java
+++ b/core/java/android/hardware/biometrics/BiometricManager.java
@@ -537,24 +537,6 @@
     }
 
     /**
-     * Listens for biometric prompt status, i.e., if it is being shown or idle.
-     * @hide
-     */
-    @RequiresPermission(USE_BIOMETRIC_INTERNAL)
-    public void registerBiometricPromptStatusListener(
-            IBiometricPromptStatusListener callback) {
-        if (mService != null) {
-            try {
-                mService.registerBiometricPromptStatusListener(callback);
-            } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
-            }
-        } else {
-            Slog.w(TAG, "registerBiometricPromptOnKeyguardCallback(): Service not connected");
-        }
-    }
-
-    /**
      * Requests all {@link Authenticators.Types#BIOMETRIC_STRONG} sensors to have their
      * authenticatorId invalidated for the specified user. This happens when enrollments have been
      * added on devices with multiple biometric sensors.
diff --git a/core/java/android/hardware/biometrics/IAuthService.aidl b/core/java/android/hardware/biometrics/IAuthService.aidl
index 8eede47..c2e5c0b 100644
--- a/core/java/android/hardware/biometrics/IAuthService.aidl
+++ b/core/java/android/hardware/biometrics/IAuthService.aidl
@@ -17,7 +17,6 @@
 package android.hardware.biometrics;
 
 import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback;
-import android.hardware.biometrics.IBiometricPromptStatusListener;
 import android.hardware.biometrics.IBiometricServiceReceiver;
 import android.hardware.biometrics.IInvalidationCallback;
 import android.hardware.biometrics.ITestSession;
@@ -64,9 +63,6 @@
     // Register callback for when keyguard biometric eligibility changes.
     void registerEnabledOnKeyguardCallback(IBiometricEnabledOnKeyguardCallback callback);
 
-    // Register callback to check biometric prompt status.
-    void registerBiometricPromptStatusListener(IBiometricPromptStatusListener callback);
-
     // Requests all BIOMETRIC_STRONG sensors to have their authenticatorId invalidated for the
     // specified user. This happens when enrollments have been added on devices with multiple
     // biometric sensors.
diff --git a/core/java/android/hardware/biometrics/IBiometricService.aidl b/core/java/android/hardware/biometrics/IBiometricService.aidl
index 36606a1..18c8d1b 100644
--- a/core/java/android/hardware/biometrics/IBiometricService.aidl
+++ b/core/java/android/hardware/biometrics/IBiometricService.aidl
@@ -17,7 +17,6 @@
 package android.hardware.biometrics;
 
 import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback;
-import android.hardware.biometrics.IBiometricPromptStatusListener;
 import android.hardware.biometrics.IBiometricServiceReceiver;
 import android.hardware.biometrics.IBiometricAuthenticator;
 import android.hardware.biometrics.IInvalidationCallback;
@@ -69,10 +68,6 @@
     @EnforcePermission("USE_BIOMETRIC_INTERNAL")
     void registerEnabledOnKeyguardCallback(IBiometricEnabledOnKeyguardCallback callback);
 
-    // Register a callback for biometric prompt status on keyguard.
-    @EnforcePermission("USE_BIOMETRIC_INTERNAL")
-    void registerBiometricPromptStatusListener(IBiometricPromptStatusListener callback);
-
     // Notify BiometricService when <Biometric>Service is ready to start the prepared client.
     // Client lifecycle is still managed in <Biometric>Service.
     @EnforcePermission("USE_BIOMETRIC_INTERNAL")
diff --git a/core/java/android/hardware/camera2/params/OutputConfiguration.java b/core/java/android/hardware/camera2/params/OutputConfiguration.java
index 5e53373..95526a8 100644
--- a/core/java/android/hardware/camera2/params/OutputConfiguration.java
+++ b/core/java/android/hardware/camera2/params/OutputConfiguration.java
@@ -1403,6 +1403,7 @@
                 if (mSurfaces.get(i) != other.mSurfaces.get(i))
                     return false;
             }
+            if (!mIsDeferredConfig && mSurfaces.size() != other.mSurfaces.size()) return false;
             if (mDynamicRangeProfile != other.mDynamicRangeProfile) {
                 return false;
             }
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index aeddd0c..8e49c4c 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -46,6 +46,7 @@
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.util.Log;
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -71,7 +72,11 @@
 @SystemService(Context.DISPLAY_SERVICE)
 public final class DisplayManager {
     private static final String TAG = "DisplayManager";
-    private static final boolean DEBUG = false;
+
+    // To enable these logs, run:
+    // 'adb shell setprop persist.log.tag.DisplayManager DEBUG && adb reboot'
+    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG)
+            || Log.isLoggable("DisplayManager_All", Log.DEBUG);
     private static final boolean ENABLE_VIRTUAL_DISPLAY_REFRESH_RATE = true;
 
     /**
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index 2b5f5ee..75f0ceb 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -42,7 +42,6 @@
 import android.os.HandlerExecutor;
 import android.os.IBinder;
 import android.os.Looper;
-import android.os.Message;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.Trace;
@@ -82,7 +81,9 @@
     private static String sCurrentPackageName = ActivityThread.currentPackageName();
     private static boolean sExtraDisplayListenerLogging = initExtraLogging();
 
-    private static final boolean DEBUG = false || sExtraDisplayListenerLogging;
+    // To enable these logs, run:
+    // 'adb shell setprop persist.log.tag.DisplayManager DEBUG && adb reboot'
+    private static final boolean DEBUG = DisplayManager.DEBUG || sExtraDisplayListenerLogging;
 
     // True if display info and display ids should be cached.
     //
@@ -412,6 +413,18 @@
         }
     }
 
+    /**
+     * Called when there is a display-related window configuration change. Reroutes the event from
+     * WindowManager to make sure the {@link Display} fields are up-to-date in the last callback.
+     * @param displayId the logical display that was changed.
+     */
+    public void handleDisplayChangeFromWindowManager(int displayId) {
+        // There can be racing condition between DMS and WMS callbacks, so force triggering the
+        // listener to make sure the client can get the onDisplayChanged callback even if
+        // DisplayInfo is not changed (Display read from both DisplayInfo and WindowConfiguration).
+        handleDisplayEvent(displayId, EVENT_DISPLAY_CHANGED, true /* forceUpdate */);
+    }
+
     private static Looper getLooperForHandler(@Nullable Handler handler) {
         Looper looper = handler != null ? handler.getLooper() : Looper.myLooper();
         if (looper == null) {
@@ -470,7 +483,7 @@
         }
     }
 
-    private void handleDisplayEvent(int displayId, @DisplayEvent int event) {
+    private void handleDisplayEvent(int displayId, @DisplayEvent int event, boolean forceUpdate) {
         final DisplayInfo info;
         synchronized (mLock) {
             if (USE_CACHE) {
@@ -501,7 +514,7 @@
         // Accepting an Executor means the listener may be synchronously invoked, so we must
         // not be holding mLock when we do so
         for (DisplayListenerDelegate listener : mDisplayListeners) {
-            listener.sendDisplayEvent(displayId, event, info);
+            listener.sendDisplayEvent(displayId, event, info, forceUpdate);
         }
     }
 
@@ -1176,7 +1189,7 @@
                 Log.d(TAG, "onDisplayEvent: displayId=" + displayId + ", event=" + eventToString(
                         event));
             }
-            handleDisplayEvent(displayId, event);
+            handleDisplayEvent(displayId, event, false /* forceUpdate */);
         }
     }
 
@@ -1197,87 +1210,86 @@
             mPackageName = packageName;
         }
 
-        public void sendDisplayEvent(int displayId, @DisplayEvent int event, DisplayInfo info) {
+        void sendDisplayEvent(int displayId, @DisplayEvent int event, @Nullable DisplayInfo info,
+                boolean forceUpdate) {
             if (extraLogging()) {
                 Slog.i(TAG, "Sending Display Event: " + eventToString(event));
             }
             long generationId = mGenerationId.get();
-            Message msg = Message.obtain(null, event, displayId, 0, info);
             mExecutor.execute(() -> {
-                // If the generation id's don't match we were canceled but still need to recycle()
+                // If the generation id's don't match we were canceled
                 if (generationId == mGenerationId.get()) {
-                    handleMessage(msg);
+                    handleDisplayEventInner(displayId, event, info, forceUpdate);
                 }
-                msg.recycle();
             });
         }
 
-        public void clearEvents() {
+        void clearEvents() {
             mGenerationId.incrementAndGet();
         }
 
-        public void setEventsMask(@EventsMask long newEventsMask) {
+        void setEventsMask(@EventsMask long newEventsMask) {
             mEventsMask = newEventsMask;
         }
 
-        private void handleMessage(Message msg) {
+        private void handleDisplayEventInner(int displayId, @DisplayEvent int event,
+                @Nullable DisplayInfo info, boolean forceUpdate) {
             if (extraLogging()) {
-                Slog.i(TAG, "DLD(" + eventToString(msg.what)
-                        + ", display=" + msg.arg1
+                Slog.i(TAG, "DLD(" + eventToString(event)
+                        + ", display=" + displayId
                         + ", mEventsMask=" + Long.toBinaryString(mEventsMask)
                         + ", mPackageName=" + mPackageName
-                        + ", msg.obj=" + msg.obj
+                        + ", displayInfo=" + info
                         + ", listener=" + mListener.getClass() + ")");
             }
             if (DEBUG) {
                 Trace.beginSection(
                         TextUtils.trimToSize(
-                                "DLD(" + eventToString(msg.what)
-                                + ", display=" + msg.arg1
+                                "DLD(" + eventToString(event)
+                                + ", display=" + displayId
                                 + ", listener=" + mListener.getClass() + ")", 127));
             }
-            switch (msg.what) {
+            switch (event) {
                 case EVENT_DISPLAY_ADDED:
                     if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_ADDED) != 0) {
-                        mListener.onDisplayAdded(msg.arg1);
+                        mListener.onDisplayAdded(displayId);
                     }
                     break;
                 case EVENT_DISPLAY_CHANGED:
                     if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_CHANGED) != 0) {
-                        DisplayInfo newInfo = (DisplayInfo) msg.obj;
-                        if (newInfo != null && !newInfo.equals(mDisplayInfo)) {
+                        if (info != null && (forceUpdate || !info.equals(mDisplayInfo))) {
                             if (extraLogging()) {
                                 Slog.i(TAG, "Sending onDisplayChanged: Display Changed. Info: "
-                                        + newInfo);
+                                        + info);
                             }
-                            mDisplayInfo.copyFrom(newInfo);
-                            mListener.onDisplayChanged(msg.arg1);
+                            mDisplayInfo.copyFrom(info);
+                            mListener.onDisplayChanged(displayId);
                         }
                     }
                     break;
                 case EVENT_DISPLAY_BRIGHTNESS_CHANGED:
                     if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS) != 0) {
-                        mListener.onDisplayChanged(msg.arg1);
+                        mListener.onDisplayChanged(displayId);
                     }
                     break;
                 case EVENT_DISPLAY_REMOVED:
                     if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_REMOVED) != 0) {
-                        mListener.onDisplayRemoved(msg.arg1);
+                        mListener.onDisplayRemoved(displayId);
                     }
                     break;
                 case EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED:
                     if ((mEventsMask & DisplayManager.EVENT_FLAG_HDR_SDR_RATIO_CHANGED) != 0) {
-                        mListener.onDisplayChanged(msg.arg1);
+                        mListener.onDisplayChanged(displayId);
                     }
                     break;
                 case EVENT_DISPLAY_CONNECTED:
                     if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0) {
-                        mListener.onDisplayConnected(msg.arg1);
+                        mListener.onDisplayConnected(displayId);
                     }
                     break;
                 case EVENT_DISPLAY_DISCONNECTED:
                     if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0) {
-                        mListener.onDisplayDisconnected(msg.arg1);
+                        mListener.onDisplayDisconnected(displayId);
                     }
                     break;
             }
@@ -1423,12 +1435,10 @@
             sExtraDisplayListenerLogging = !TextUtils.isEmpty(EXTRA_LOGGING_PACKAGE_NAME)
                     && EXTRA_LOGGING_PACKAGE_NAME.equals(sCurrentPackageName);
         }
-        // TODO: b/306170135 - return sExtraDisplayListenerLogging instead
-        return true;
+        return sExtraDisplayListenerLogging;
     }
 
     private static boolean extraLogging() {
-        // TODO: b/306170135 - return sExtraDisplayListenerLogging & package name check instead
-        return true;
+        return sExtraDisplayListenerLogging;
     }
 }
diff --git a/core/java/android/net/vcn/flags.aconfig b/core/java/android/net/vcn/flags.aconfig
new file mode 100644
index 0000000..6956916
--- /dev/null
+++ b/core/java/android/net/vcn/flags.aconfig
@@ -0,0 +1,8 @@
+package: "android.net.vcn"
+
+flag {
+    name: "safe_mode_config"
+    namespace: "vcn"
+    description: "Feature flag for safe mode configurability"
+    bug: "276358140"
+}
\ No newline at end of file
diff --git a/core/java/android/os/BugreportManager.java b/core/java/android/os/BugreportManager.java
index 58def6e..960e84d 100644
--- a/core/java/android/os/BugreportManager.java
+++ b/core/java/android/os/BugreportManager.java
@@ -26,6 +26,7 @@
 import android.annotation.SuppressAutoDoc;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
+import android.annotation.UserHandleAware;
 import android.annotation.WorkerThread;
 import android.app.ActivityManager;
 import android.content.Context;
@@ -280,8 +281,8 @@
      *
      * <p>{@link BugreportManager} takes ownership of {@code bugreportFd}.
      *
-     * <p>The caller may only request to retrieve a given bugreport once. Subsequent calls will fail
-     * with error code {@link BugreportCallback#BUGREPORT_ERROR_NO_BUGREPORT_TO_RETRIEVE}.
+     * <p>The caller can reattempt to retrieve the bugreport multiple times if the user has not
+     * consented on previous attempts.
      *
      * @param bugreportFile the identifier for a bugreport that was previously generated for this
      *      caller using {@code startBugreport}.
@@ -294,6 +295,7 @@
     @SystemApi
     @RequiresPermission(Manifest.permission.DUMP)
     @WorkerThread
+    @UserHandleAware
     public void retrieveBugreport(
             @NonNull String bugreportFile,
             @NonNull ParcelFileDescriptor bugreportFd,
@@ -307,8 +309,10 @@
             Preconditions.checkNotNull(callback);
             DumpstateListener dsListener = new DumpstateListener(executor, callback, false, false);
             mBinder.retrieveBugreport(Binder.getCallingUid(), mContext.getOpPackageName(),
+                    mContext.getUserId(),
                     bugreportFd.getFileDescriptor(),
                     bugreportFile,
+                    /* keepBugreportOnRetrieval = */ false,
                     dsListener);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
diff --git a/core/java/android/os/BugreportParams.java b/core/java/android/os/BugreportParams.java
index e8ad303..8510084 100644
--- a/core/java/android/os/BugreportParams.java
+++ b/core/java/android/os/BugreportParams.java
@@ -20,6 +20,7 @@
 import android.annotation.IntDef;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
+import android.app.admin.flags.Flags;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -128,6 +129,8 @@
      *
      * @hide
      */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_ONBOARDING_BUGREPORT_V2_ENABLED)
     public static final int BUGREPORT_MODE_ONBOARDING = IDumpstate.BUGREPORT_MODE_ONBOARDING;
 
     /**
@@ -145,7 +148,8 @@
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(flag = true, prefix = { "BUGREPORT_FLAG_" }, value = {
             BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA,
-            BUGREPORT_FLAG_DEFER_CONSENT
+            BUGREPORT_FLAG_DEFER_CONSENT,
+            BUGREPORT_FLAG_KEEP_BUGREPORT_ON_RETRIEVAL
     })
     public @interface BugreportFlag {}
 
@@ -165,4 +169,20 @@
      * String, ParcelFileDescriptor, Executor, BugreportManager.BugreportCallback)}.
      */
     public static final int BUGREPORT_FLAG_DEFER_CONSENT = IDumpstate.BUGREPORT_FLAG_DEFER_CONSENT;
+
+    /**
+     * Flag for keeping a bugreport stored even after it has been retrieved via
+     * {@link BugreportManager#retrieveBugreport}.
+     *
+     * <p>This flag can only be used when {@link #BUGREPORT_FLAG_DEFER_CONSENT} is set.
+     * The bugreport may be retrieved multiple times using
+     * {@link BugreportManager#retrieveBugreport(
+     * String, ParcelFileDescriptor, Executor, BugreportManager.BugreportCallback)}.
+     *
+     * @hide
+     */
+    @TestApi
+    @FlaggedApi(Flags.FLAG_ONBOARDING_BUGREPORT_V2_ENABLED)
+    public static final int BUGREPORT_FLAG_KEEP_BUGREPORT_ON_RETRIEVAL =
+            IDumpstate.BUGREPORT_FLAG_KEEP_BUGREPORT_ON_RETRIEVAL;
 }
diff --git a/core/java/android/os/PerformanceHintManager.java b/core/java/android/os/PerformanceHintManager.java
index cbc9213..11084b8 100644
--- a/core/java/android/os/PerformanceHintManager.java
+++ b/core/java/android/os/PerformanceHintManager.java
@@ -16,6 +16,7 @@
 
 package android.os;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -231,6 +232,7 @@
          *
          * @param enabled The flag that sets whether this session uses power-efficient scheduling.
          */
+        @FlaggedApi(Flags.FLAG_ADPF_PREFER_POWER_EFFICIENCY)
         public void setPreferPowerEfficiency(boolean enabled) {
             nativeSetPreferPowerEfficiency(mNativeSessionPtr, enabled);
         }
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index fce715a..d2c1755 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -16,6 +16,7 @@
 
 package android.os;
 
+import android.annotation.FlaggedApi;
 import android.Manifest.permission;
 import android.annotation.CallbackExecutor;
 import android.annotation.CurrentTimeMillisLong;
@@ -1940,6 +1941,7 @@
      *
      * @hide
      */
+    @FlaggedApi(android.os.Flags.FLAG_BATTERY_SAVER_SUPPORTED_CHECK_API)
     @TestApi
     public boolean isBatterySaverSupported() {
         try {
diff --git a/core/java/android/os/StrictMode.java b/core/java/android/os/StrictMode.java
index 81d4e3a..47b6d8d 100644
--- a/core/java/android/os/StrictMode.java
+++ b/core/java/android/os/StrictMode.java
@@ -2367,14 +2367,14 @@
     }
 
     /** Assume locked until we hear otherwise */
-    private static volatile boolean sUserKeyUnlocked = false;
+    private static volatile boolean sCeStorageUnlocked = false;
 
-    private static boolean isUserKeyUnlocked(int userId) {
+    private static boolean isCeStorageUnlocked(int userId) {
         final IStorageManager storage = IStorageManager.Stub
                 .asInterface(ServiceManager.getService("mount"));
         if (storage != null) {
             try {
-                return storage.isUserKeyUnlocked(userId);
+                return storage.isCeStorageUnlocked(userId);
             } catch (RemoteException ignored) {
             }
         }
@@ -2387,13 +2387,13 @@
         // since any relocking of that user will always result in our
         // process being killed to release any CE FDs we're holding onto.
         if (userId == UserHandle.myUserId()) {
-            if (sUserKeyUnlocked) {
+            if (sCeStorageUnlocked) {
                 return;
-            } else if (isUserKeyUnlocked(userId)) {
-                sUserKeyUnlocked = true;
+            } else if (isCeStorageUnlocked(userId)) {
+                sCeStorageUnlocked = true;
                 return;
             }
-        } else if (isUserKeyUnlocked(userId)) {
+        } else if (isCeStorageUnlocked(userId)) {
             return;
         }
 
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index c4521c0..86f03cd 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -34,3 +34,17 @@
     description: "Introduce a constant as maximum value of bugreport mode."
     bug: "305067125"
 }
+
+flag {
+    name: "adpf_prefer_power_efficiency"
+    namespace: "game"
+    description: "Guards the ADPF power efficiency API"
+    bug: "288117936"
+}
+
+flag {
+    name: "battery_saver_supported_check_api"
+    namespace: "backstage_power"
+    description: "Guards a new API in PowerManager to check if battery saver is supported or not."
+    bug: "305067031"
+}
\ No newline at end of file
diff --git a/core/java/android/os/storage/IStorageManager.aidl b/core/java/android/os/storage/IStorageManager.aidl
index 369a193..3ecf74e 100644
--- a/core/java/android/os/storage/IStorageManager.aidl
+++ b/core/java/android/os/storage/IStorageManager.aidl
@@ -134,20 +134,20 @@
     @EnforcePermission("MOUNT_UNMOUNT_FILESYSTEMS")
     void setDebugFlags(int flags, int mask) = 60;
     @EnforcePermission("STORAGE_INTERNAL")
-    void createUserKey(int userId, int serialNumber, boolean ephemeral) = 61;
+    void createUserStorageKeys(int userId, int serialNumber, boolean ephemeral) = 61;
     @EnforcePermission("STORAGE_INTERNAL")
-    void destroyUserKey(int userId) = 62;
+    void destroyUserStorageKeys(int userId) = 62;
     @EnforcePermission("STORAGE_INTERNAL")
-    void unlockUserKey(int userId, int serialNumber, in byte[] secret) = 63;
+    void unlockCeStorage(int userId, int serialNumber, in byte[] secret) = 63;
     @EnforcePermission("STORAGE_INTERNAL")
-    void lockUserKey(int userId) = 64;
-    boolean isUserKeyUnlocked(int userId) = 65;
+    void lockCeStorage(int userId) = 64;
+    boolean isCeStorageUnlocked(int userId) = 65;
     @EnforcePermission("STORAGE_INTERNAL")
     void prepareUserStorage(in String volumeUuid, int userId, int serialNumber, int flags) = 66;
     @EnforcePermission("STORAGE_INTERNAL")
     void destroyUserStorage(in String volumeUuid, int userId, int flags) = 67;
     @EnforcePermission("STORAGE_INTERNAL")
-    void setUserKeyProtection(int userId, in byte[] secret) = 70;
+    void setCeStorageProtection(int userId, in byte[] secret) = 70;
     @EnforcePermission("MOUNT_FORMAT_FILESYSTEMS")
     void fstrim(int flags, IVoldTaskListener listener) = 72;
     AppFuseMount mountProxyFileDescriptorBridge() = 73;
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index ee387e7..2d1802a 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -1589,28 +1589,64 @@
                 DEFAULT_FULL_THRESHOLD_BYTES);
     }
 
-    /** {@hide} */
-    public void createUserKey(int userId, int serialNumber, boolean ephemeral) {
+    /**
+     * Creates the keys for a user's credential-encrypted (CE) and device-encrypted (DE) storage.
+     * <p>
+     * This creates the user's CE key and DE key for internal storage, then adds them to the kernel.
+     * Then, if the user is not ephemeral, this stores the DE key (encrypted) on flash.  (The CE key
+     * is not stored until {@link IStorageManager#setCeStorageProtection()}.)
+     * <p>
+     * This does not create the CE and DE directories themselves.  For that, see {@link
+     * #prepareUserStorage()}.
+     * <p>
+     * This is only intended to be called by UserManagerService, as part of creating a user.
+     *
+     * @param userId ID of the user
+     * @param serialNumber serial number of the user
+     * @param ephemeral whether the user is ephemeral
+     * @throws RuntimeException on error.  The user's keys already existing is considered an error.
+     * @hide
+     */
+    public void createUserStorageKeys(int userId, int serialNumber, boolean ephemeral) {
         try {
-            mStorageManager.createUserKey(userId, serialNumber, ephemeral);
+            mStorageManager.createUserStorageKeys(userId, serialNumber, ephemeral);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
     }
 
-    /** {@hide} */
-    public void destroyUserKey(int userId) {
+    /**
+     * Destroys the keys for a user's credential-encrypted (CE) and device-encrypted (DE) storage.
+     * <p>
+     * This evicts the keys from the kernel (if present), which "locks" the corresponding
+     * directories.  Then, this deletes the encrypted keys from flash.  This operates on all the
+     * user's CE and DE keys, for both internal and adoptable storage.
+     * <p>
+     * This does not destroy the CE and DE directories themselves.  For that, see {@link
+     * #destroyUserStorage()}.
+     * <p>
+     * This is only intended to be called by UserManagerService, as part of removing a user.
+     *
+     * @param userId ID of the user
+     * @throws RuntimeException on error.  On error, as many things as possible are still destroyed.
+     * @hide
+     */
+    public void destroyUserStorageKeys(int userId) {
         try {
-            mStorageManager.destroyUserKey(userId);
+            mStorageManager.destroyUserStorageKeys(userId);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
     }
 
-    /** {@hide} */
-    public void lockUserKey(int userId) {
+    /**
+     * Locks the user's credential-encrypted (CE) storage.
+     *
+     * @hide
+     */
+    public void lockCeStorage(int userId) {
         try {
-            mStorageManager.lockUserKey(userId);
+            mStorageManager.lockCeStorage(userId);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1637,17 +1673,26 @@
     /** {@hide} */
     @TestApi
     public static boolean isUserKeyUnlocked(int userId) {
+        return isCeStorageUnlocked(userId);
+    }
+
+    /**
+     * Returns true if the user's credential-encrypted (CE) storage is unlocked.
+     *
+     * @hide
+     */
+    public static boolean isCeStorageUnlocked(int userId) {
         if (sStorageManager == null) {
             sStorageManager = IStorageManager.Stub
                     .asInterface(ServiceManager.getService("mount"));
         }
         if (sStorageManager == null) {
-            Slog.w(TAG, "Early during boot, assuming locked");
+            Slog.w(TAG, "Early during boot, assuming CE storage is locked");
             return false;
         }
         final long token = Binder.clearCallingIdentity();
         try {
-            return sStorageManager.isUserKeyUnlocked(userId);
+            return sStorageManager.isCeStorageUnlocked(userId);
         } catch (RemoteException e) {
             throw e.rethrowAsRuntimeException();
         } finally {
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index 3f06a91..0798f65 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -35,3 +35,10 @@
     description: "enable the shouldRegisterAttributionSource API"
     bug: "305057691"
 }
+
+flag {
+  name: "attribution_source_constructor"
+  namespace: "permissions"
+  description: "enable AttributionSource(int, int, String, String, IBinder, String[], AttributionSource)"
+  bug: "304478648"
+}
\ No newline at end of file
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 4bb401a..a2f1ce1e 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -12067,6 +12067,13 @@
         public static final String DND_CONFIGS_MIGRATED = "dnd_settings_migrated";
 
         /**
+         * Controls whether to hide private space entry point in All Apps
+         *
+         * @hide
+         */
+        public static final String HIDE_PRIVATESPACE_ENTRY_POINT = "hide_privatespace_entry_point";
+
+        /**
          * These entries are considered common between the personal and the managed profile,
          * since the managed profile doesn't get to change them.
          */
diff --git a/core/java/android/service/notification/flags.aconfig b/core/java/android/service/notification/flags.aconfig
index 2931435..52d4d47 100644
--- a/core/java/android/service/notification/flags.aconfig
+++ b/core/java/android/service/notification/flags.aconfig
@@ -7,3 +7,11 @@
   bug: "284297289"
 }
 
+flag {
+  name: "notification_lifetime_extension_refactor"
+    namespace: "systemui"
+    description: "Enables moving notification lifetime extension management from SystemUI to "
+        "Notification Manager Service"
+    bug: "299448097"
+}
+
diff --git a/core/java/android/service/voice/HotwordTrainingDataLimitEnforcer.java b/core/java/android/service/voice/HotwordTrainingDataLimitEnforcer.java
index 76e506c..7eb5280 100644
--- a/core/java/android/service/voice/HotwordTrainingDataLimitEnforcer.java
+++ b/core/java/android/service/voice/HotwordTrainingDataLimitEnforcer.java
@@ -27,10 +27,9 @@
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.io.File;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.Locale;
-import java.util.TimeZone;
+import java.time.LocalDate;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
 
 /**
  * Enforces daily limits on the egress of {@link HotwordTrainingData} from the hotword detection
@@ -111,9 +110,9 @@
     }
 
     private boolean incrementTrainingDataEgressCountLocked() {
-        SimpleDateFormat dt = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
-        dt.setTimeZone(TimeZone.getTimeZone("UTC"));
-        String currentDate = dt.format(new Date());
+        LocalDate utcDate = LocalDate.now(ZoneOffset.UTC);
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+        String currentDate = utcDate.format(formatter);
 
         String storedDate = mSharedPreferences.getString(TRAINING_DATA_EGRESS_DATE, "");
         int storedCount = mSharedPreferences.getInt(TRAINING_DATA_EGRESS_COUNT, 0);
diff --git a/core/java/android/view/LayoutInflater.java b/core/java/android/view/LayoutInflater.java
index 99a7fe5..aad3bf2 100644
--- a/core/java/android/view/LayoutInflater.java
+++ b/core/java/android/view/LayoutInflater.java
@@ -20,11 +20,9 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemService;
-import android.annotation.TestApi;
 import android.annotation.UiContext;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
-import android.content.pm.ApplicationInfo;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
@@ -42,15 +40,11 @@
 
 import com.android.internal.R;
 
-import dalvik.system.PathClassLoader;
-
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
-import java.io.File;
 import java.io.IOException;
 import java.lang.reflect.Constructor;
-import java.lang.reflect.Method;
 import java.util.HashMap;
 import java.util.Objects;
 
@@ -82,12 +76,6 @@
     private static final String TAG = LayoutInflater.class.getSimpleName();
     private static final boolean DEBUG = false;
 
-    private static final String COMPILED_VIEW_DEX_FILE_NAME = "/compiled_view.dex";
-    /**
-     * Whether or not we use the precompiled layout.
-     */
-    private static final String USE_PRECOMPILED_LAYOUT = "view.precompiled_layout_enabled";
-
     /** Empty stack trace used to avoid log spam in re-throw exceptions. */
     private static final StackTraceElement[] EMPTY_STACK_TRACE = new StackTraceElement[0];
 
@@ -116,13 +104,6 @@
     private Factory2 mPrivateFactory;
     private Filter mFilter;
 
-    // Indicates whether we should try to inflate layouts using a precompiled layout instead of
-    // inflating from the XML resource.
-    private boolean mUseCompiledView;
-    // This variable holds the classloader that will be used to look for precompiled layouts. The
-    // The classloader includes the generated compiled_view.dex file.
-    private ClassLoader mPrecompiledClassLoader;
-
     /**
      * This is not a public API. Two APIs are now available to alleviate the need to access
      * this directly: {@link #createView(Context, String, String, AttributeSet)} and
@@ -259,7 +240,6 @@
     protected LayoutInflater(Context context) {
         StrictMode.assertConfigurationContext(context, "LayoutInflater");
         mContext = context;
-        initPrecompiledViews();
     }
 
     /**
@@ -277,7 +257,6 @@
         mFactory2 = original.mFactory2;
         mPrivateFactory = original.mPrivateFactory;
         setFilter(original.mFilter);
-        initPrecompiledViews();
     }
 
     /**
@@ -419,57 +398,6 @@
         }
     }
 
-    private void initPrecompiledViews() {
-        // Precompiled layouts are not supported in this release.
-        boolean enabled = false;
-        initPrecompiledViews(enabled);
-    }
-
-    private void initPrecompiledViews(boolean enablePrecompiledViews) {
-        mUseCompiledView = enablePrecompiledViews;
-
-        if (!mUseCompiledView) {
-            mPrecompiledClassLoader = null;
-            return;
-        }
-
-        // Make sure the application allows code generation
-        ApplicationInfo appInfo = mContext.getApplicationInfo();
-        if (appInfo.isEmbeddedDexUsed() || appInfo.isPrivilegedApp()) {
-            mUseCompiledView = false;
-            return;
-        }
-
-        // Try to load the precompiled layout file.
-        try {
-            mPrecompiledClassLoader = mContext.getClassLoader();
-            String dexFile = mContext.getCodeCacheDir() + COMPILED_VIEW_DEX_FILE_NAME;
-            if (new File(dexFile).exists()) {
-                mPrecompiledClassLoader = new PathClassLoader(dexFile, mPrecompiledClassLoader);
-            } else {
-                // If the precompiled layout file doesn't exist, then disable precompiled
-                // layouts.
-                mUseCompiledView = false;
-            }
-        } catch (Throwable e) {
-            if (DEBUG) {
-                Log.e(TAG, "Failed to initialized precompiled views layouts", e);
-            }
-            mUseCompiledView = false;
-        }
-        if (!mUseCompiledView) {
-            mPrecompiledClassLoader = null;
-        }
-    }
-
-    /**
-     * @hide for use by CTS tests
-     */
-    @TestApi
-    public void setPrecompiledLayoutsEnabledForTesting(boolean enablePrecompiledLayouts) {
-        initPrecompiledViews(enablePrecompiledLayouts);
-    }
-
     /**
      * Inflate a new view hierarchy from the specified xml resource. Throws
      * {@link InflateException} if there is an error.
@@ -529,10 +457,6 @@
                   + Integer.toHexString(resource) + ")");
         }
 
-        View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
-        if (view != null) {
-            return view;
-        }
         XmlResourceParser parser = res.getLayout(resource);
         try {
             return inflate(parser, root, attachToRoot);
@@ -541,54 +465,6 @@
         }
     }
 
-    private @Nullable
-    View tryInflatePrecompiled(@LayoutRes int resource, Resources res, @Nullable ViewGroup root,
-        boolean attachToRoot) {
-        if (!mUseCompiledView) {
-            return null;
-        }
-
-        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate (precompiled)");
-
-        // Try to inflate using a precompiled layout.
-        String pkg = res.getResourcePackageName(resource);
-        String layout = res.getResourceEntryName(resource);
-
-        try {
-            Class clazz = Class.forName("" + pkg + ".CompiledView", false, mPrecompiledClassLoader);
-            Method inflater = clazz.getMethod(layout, Context.class, int.class);
-            View view = (View) inflater.invoke(null, mContext, resource);
-
-            if (view != null && root != null) {
-                // We were able to use the precompiled inflater, but now we need to do some work to
-                // attach the view to the root correctly.
-                XmlResourceParser parser = res.getLayout(resource);
-                try {
-                    AttributeSet attrs = Xml.asAttributeSet(parser);
-                    advanceToRootNode(parser);
-                    ViewGroup.LayoutParams params = root.generateLayoutParams(attrs);
-
-                    if (attachToRoot) {
-                        root.addView(view, params);
-                    } else {
-                        view.setLayoutParams(params);
-                    }
-                } finally {
-                    parser.close();
-                }
-            }
-
-            return view;
-        } catch (Throwable e) {
-            if (DEBUG) {
-                Log.e(TAG, "Failed to use precompiled view", e);
-            }
-        } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
-        }
-        return null;
-    }
-
     /**
      * Advances the given parser to the first START_TAG. Throws InflateException if no start tag is
      * found.
@@ -1050,7 +926,7 @@
      * of the general view creation logic, and thus may return {@code null} for some tags. This
      * method is used by {@link LayoutInflater#inflate} in creating {@code View} objects.
      *
-     * @hide for use by precompiled layouts.
+     * @hide originally for internal use by precompiled layouts, which have since been removed.
      *
      * @param parent the parent view, used to inflate layout params
      * @param name the name of the XML tag used to define the view
@@ -1217,85 +1093,80 @@
                 + "reference. The layout ID " + value + " is not valid.");
         }
 
-        final View precompiled = tryInflatePrecompiled(layout, context.getResources(),
-            (ViewGroup) parent, /*attachToRoot=*/true);
-        if (precompiled == null) {
-            final XmlResourceParser childParser = context.getResources().getLayout(layout);
+        final XmlResourceParser childParser = context.getResources().getLayout(layout);
+        try {
+            final AttributeSet childAttrs = Xml.asAttributeSet(childParser);
 
-            try {
-                final AttributeSet childAttrs = Xml.asAttributeSet(childParser);
-
-                while ((type = childParser.next()) != XmlPullParser.START_TAG &&
-                    type != XmlPullParser.END_DOCUMENT) {
-                    // Empty.
-                }
-
-                if (type != XmlPullParser.START_TAG) {
-                    throw new InflateException(getParserStateDescription(context, childAttrs)
-                            + ": No start tag found!");
-                }
-
-                final String childName = childParser.getName();
-
-                if (TAG_MERGE.equals(childName)) {
-                    // The <merge> tag doesn't support android:theme, so
-                    // nothing special to do here.
-                    rInflate(childParser, parent, context, childAttrs, false);
-                } else {
-                    final View view = createViewFromTag(parent, childName,
-                        context, childAttrs, hasThemeOverride);
-                    final ViewGroup group = (ViewGroup) parent;
-
-                    final TypedArray a = context.obtainStyledAttributes(
-                        attrs, R.styleable.Include);
-                    final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
-                    final int visibility = a.getInt(R.styleable.Include_visibility, -1);
-                    a.recycle();
-
-                    // We try to load the layout params set in the <include /> tag.
-                    // If the parent can't generate layout params (ex. missing width
-                    // or height for the framework ViewGroups, though this is not
-                    // necessarily true of all ViewGroups) then we expect it to throw
-                    // a runtime exception.
-                    // We catch this exception and set localParams accordingly: true
-                    // means we successfully loaded layout params from the <include>
-                    // tag, false means we need to rely on the included layout params.
-                    ViewGroup.LayoutParams params = null;
-                    try {
-                        params = group.generateLayoutParams(attrs);
-                    } catch (RuntimeException e) {
-                        // Ignore, just fail over to child attrs.
-                    }
-                    if (params == null) {
-                        params = group.generateLayoutParams(childAttrs);
-                    }
-                    view.setLayoutParams(params);
-
-                    // Inflate all children.
-                    rInflateChildren(childParser, view, childAttrs, true);
-
-                    if (id != View.NO_ID) {
-                        view.setId(id);
-                    }
-
-                    switch (visibility) {
-                        case 0:
-                            view.setVisibility(View.VISIBLE);
-                            break;
-                        case 1:
-                            view.setVisibility(View.INVISIBLE);
-                            break;
-                        case 2:
-                            view.setVisibility(View.GONE);
-                            break;
-                    }
-
-                    group.addView(view);
-                }
-            } finally {
-                childParser.close();
+            while ((type = childParser.next()) != XmlPullParser.START_TAG
+                    && type != XmlPullParser.END_DOCUMENT) {
+                // Empty.
             }
+
+            if (type != XmlPullParser.START_TAG) {
+                throw new InflateException(getParserStateDescription(context, childAttrs)
+                        + ": No start tag found!");
+            }
+
+            final String childName = childParser.getName();
+
+            if (TAG_MERGE.equals(childName)) {
+                // The <merge> tag doesn't support android:theme, so
+                // nothing special to do here.
+                rInflate(childParser, parent, context, childAttrs, false);
+            } else {
+                final View view =
+                        createViewFromTag(parent, childName, context, childAttrs, hasThemeOverride);
+                final ViewGroup group = (ViewGroup) parent;
+
+                final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Include);
+                final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
+                final int visibility = a.getInt(R.styleable.Include_visibility, -1);
+                a.recycle();
+
+                // We try to load the layout params set in the <include /> tag.
+                // If the parent can't generate layout params (ex. missing width
+                // or height for the framework ViewGroups, though this is not
+                // necessarily true of all ViewGroups) then we expect it to throw
+                // a runtime exception.
+                // We catch this exception and set localParams accordingly: true
+                // means we successfully loaded layout params from the <include>
+                // tag, false means we need to rely on the included layout params.
+                ViewGroup.LayoutParams params = null;
+                try {
+                    params = group.generateLayoutParams(attrs);
+                } catch (RuntimeException e) {
+                    // Ignore, just fail over to child attrs.
+                }
+                if (params == null) {
+                    params = group.generateLayoutParams(childAttrs);
+                }
+                view.setLayoutParams(params);
+
+                // Inflate all children.
+                rInflateChildren(childParser, view, childAttrs, true);
+
+                if (id != View.NO_ID) {
+                    view.setId(id);
+                }
+
+                switch (visibility) {
+                    case 0:
+                        view.setVisibility(View.VISIBLE);
+                        break;
+                    case 1:
+                        view.setVisibility(View.INVISIBLE);
+                        break;
+                    case 2:
+                        view.setVisibility(View.GONE);
+                        break;
+                }
+
+                group.addView(view);
+            }
+        } finally {
+            childParser.close();
         }
+
         LayoutInflater.consumeChildElements(parser);
     }
 
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index dfada58..4da02f9 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -1020,8 +1020,7 @@
         mDisplay = display;
         mBasePackageName = context.getBasePackageName();
         final String name = DisplayProperties.debug_vri_package().orElse(null);
-        // TODO: b/306170135 - return to using textutils check on package name.
-        mExtraDisplayListenerLogging = true;
+        mExtraDisplayListenerLogging = !TextUtils.isEmpty(name) && name.equals(mBasePackageName);
         mThread = Thread.currentThread();
         mLocation = new WindowLeaked(null);
         mLocation.fillInStackTrace();
@@ -11613,7 +11612,14 @@
             Log.d(mTag, "registerCallbacksForSync syncBuffer=" + syncBuffer);
         }
 
-        surfaceSyncGroup.addTransaction(mPendingTransaction);
+        final Transaction t;
+        if (mHasPendingTransactions) {
+            t = new Transaction();
+            t.merge(mPendingTransaction);
+        } else {
+            t = null;
+        }
+
         mAttachInfo.mThreadedRenderer.registerRtFrameCallback(new FrameDrawingCallback() {
             @Override
             public void onFrameDraw(long frame) {
@@ -11626,6 +11632,9 @@
                             "Received frameDrawingCallback syncResult=" + syncResult + " frameNum="
                                     + frame + ".");
                 }
+                if (t != null) {
+                    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
diff --git a/core/java/android/window/TaskFragmentParentInfo.java b/core/java/android/window/TaskFragmentParentInfo.java
index 841354a..e6eeca4 100644
--- a/core/java/android/window/TaskFragmentParentInfo.java
+++ b/core/java/android/window/TaskFragmentParentInfo.java
@@ -35,17 +35,21 @@
 
     private final boolean mVisible;
 
+    private final boolean mHasDirectActivity;
+
     public TaskFragmentParentInfo(@NonNull Configuration configuration, int displayId,
-            boolean visible) {
+            boolean visible, boolean hasDirectActivity) {
         mConfiguration.setTo(configuration);
         mDisplayId = displayId;
         mVisible = visible;
+        mHasDirectActivity = hasDirectActivity;
     }
 
     public TaskFragmentParentInfo(@NonNull TaskFragmentParentInfo info) {
         mConfiguration.setTo(info.getConfiguration());
         mDisplayId = info.mDisplayId;
         mVisible = info.mVisible;
+        mHasDirectActivity = info.mHasDirectActivity;
     }
 
     /** The {@link Configuration} of the parent Task */
@@ -68,6 +72,14 @@
     }
 
     /**
+     * Whether the parent Task has any direct child activity, which is not embedded in any
+     * TaskFragment, or not
+     */
+    public boolean hasDirectActivity() {
+        return mHasDirectActivity;
+    }
+
+    /**
      * Returns {@code true} if the parameters which are important for task fragment
      * organizers are equal between this {@link TaskFragmentParentInfo} and {@code that}.
      * Note that this method is usually called with
@@ -80,7 +92,7 @@
             return false;
         }
         return getWindowingMode() == that.getWindowingMode() && mDisplayId == that.mDisplayId
-                && mVisible == that.mVisible;
+                && mVisible == that.mVisible && mHasDirectActivity == that.mHasDirectActivity;
     }
 
     @WindowConfiguration.WindowingMode
@@ -94,6 +106,7 @@
                 + "config=" + mConfiguration
                 + ", displayId=" + mDisplayId
                 + ", visible=" + mVisible
+                + ", hasDirectActivity=" + mHasDirectActivity
                 + "}";
     }
 
@@ -114,7 +127,8 @@
         final TaskFragmentParentInfo that = (TaskFragmentParentInfo) obj;
         return mConfiguration.equals(that.mConfiguration)
                 && mDisplayId == that.mDisplayId
-                && mVisible == that.mVisible;
+                && mVisible == that.mVisible
+                && mHasDirectActivity == that.mHasDirectActivity;
     }
 
     @Override
@@ -122,6 +136,7 @@
         int result = mConfiguration.hashCode();
         result = 31 * result + mDisplayId;
         result = 31 * result + (mVisible ? 1 : 0);
+        result = 31 * result + (mHasDirectActivity ? 1 : 0);
         return result;
     }
 
@@ -130,12 +145,14 @@
         mConfiguration.writeToParcel(dest, flags);
         dest.writeInt(mDisplayId);
         dest.writeBoolean(mVisible);
+        dest.writeBoolean(mHasDirectActivity);
     }
 
     private TaskFragmentParentInfo(Parcel in) {
         mConfiguration.readFromParcel(in);
         mDisplayId = in.readInt();
         mVisible = in.readBoolean();
+        mHasDirectActivity = in.readBoolean();
     }
 
     public static final Creator<TaskFragmentParentInfo> CREATOR =
diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java
index c2b5196..d9b5b2d 100644
--- a/core/java/android/window/WindowContainerTransaction.java
+++ b/core/java/android/window/WindowContainerTransaction.java
@@ -804,7 +804,7 @@
      * Sets/removes the always on top flag for this {@code windowContainer}. See
      * {@link com.android.server.wm.ConfigurationContainer#setAlwaysOnTop(boolean)}.
      * Please note that this method is only intended to be used for a
-     * {@link com.android.server.wm.DisplayArea}.
+     * {@link com.android.server.wm.Task} or {@link com.android.server.wm.DisplayArea}.
      *
      * <p>
      *     Setting always on top to {@code True} will also make the {@code windowContainer} to move
diff --git a/core/java/android/window/WindowProviderService.java b/core/java/android/window/WindowProviderService.java
index 0cbfcc5..c81c9ec 100644
--- a/core/java/android/window/WindowProviderService.java
+++ b/core/java/android/window/WindowProviderService.java
@@ -129,7 +129,6 @@
     @SuppressLint({"OnNameExpected", "ExecutorRegistration"})
     // Suppress lint because this is a legacy named function and doesn't have an optional param
     // for executor.
-    // TODO(b/259347943): Update documentation for U.
     /**
      * Here we override to prevent WindowProviderService from invoking
      * {@link Application.registerComponentCallback}, which will result in callback registered
diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java
index 0399430..7d78f29 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistory.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistory.java
@@ -603,6 +603,17 @@
     }
 
     /**
+     * Returns the monotonic clock time when the available battery history collection started.
+     */
+    public long getStartTime() {
+        if (!mHistoryFiles.isEmpty()) {
+            return mHistoryFiles.get(0).monotonicTimeMs;
+        } else {
+            return mHistoryBufferStartTime;
+        }
+    }
+
+    /**
      * Start iterating history files and history buffer.
      *
      * @param startTimeMs monotonic time (the HistoryItem.time field) to start iterating from,
diff --git a/core/java/com/android/internal/os/MultiStateStats.java b/core/java/com/android/internal/os/MultiStateStats.java
index dc5055a..ecfed53 100644
--- a/core/java/com/android/internal/os/MultiStateStats.java
+++ b/core/java/com/android/internal/os/MultiStateStats.java
@@ -29,6 +29,8 @@
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.Arrays;
+import java.util.function.Consumer;
+import java.util.function.Function;
 
 /**
  * Maintains multidimensional multi-state stats.  States could be something like on-battery (0,1),
@@ -52,13 +54,55 @@
 
         public States(String name, boolean tracked, String... labels) {
             mName = name;
-            this.mTracked = tracked;
-            this.mLabels = labels;
+            mTracked = tracked;
+            mLabels = labels;
         }
 
         public boolean isTracked() {
             return mTracked;
         }
+
+        public String getName() {
+            return mName;
+        }
+
+        public String[] getLabels() {
+            return mLabels;
+        }
+
+        /**
+         * Iterates over all combinations of tracked states and invokes <code>consumer</code>
+         * for each of them.
+         */
+        public static void forEachTrackedStateCombination(States[] states,
+                Consumer<int[]> consumer) {
+            forEachTrackedStateCombination(consumer, states, new int[states.length], 0);
+        }
+
+        /**
+         * Recursive function that does a depth-first traversal of the multi-dimensional
+         * state space. Each time the traversal reaches the end of the <code>states</code> array,
+         * <code>statesValues</code> contains a unique combination of values for all tracked states.
+         * For untracked states, the corresponding values are left as 0.  The end result is
+         * that the <code>consumer</code> is invoked for every unique combination of tracked state
+         * values.  For example, it may be a sequence of calls like screen-on/power-on,
+         * screen-on/power-off, screen-off/power-on, screen-off/power-off.
+         */
+        private static void forEachTrackedStateCombination(Consumer<int[]> consumer,
+                States[] states, int[] statesValues, int stateIndex) {
+            if (stateIndex < statesValues.length) {
+                if (!states[stateIndex].mTracked) {
+                    forEachTrackedStateCombination(consumer, states, statesValues, stateIndex + 1);
+                    return;
+                }
+                for (int i = 0; i < states[stateIndex].mLabels.length; i++) {
+                    statesValues[stateIndex] = i;
+                    forEachTrackedStateCombination(consumer, states, statesValues, stateIndex + 1);
+                }
+                return;
+            }
+            consumer.accept(statesValues);
+        }
     }
 
     /**
@@ -276,6 +320,13 @@
     }
 
     /**
+     * Updates the stats values for the provided combination of states.
+     */
+    public void setStats(int[] states, long[] values) {
+        mCounter.setValues(mFactory.getSerialState(states), values);
+    }
+
+    /**
      * Resets the counters.
      */
     public void reset() {
@@ -293,24 +344,27 @@
      */
     public void writeXml(TypedXmlSerializer serializer) throws IOException {
         long[] tmpArray = new long[mCounter.getArrayLength()];
-        writeXmlAllStates(serializer, new int[mFactory.mStates.length], 0, tmpArray);
+
+        try {
+            States.forEachTrackedStateCombination(mFactory.mStates,
+                    states -> {
+                        try {
+                            writeXmlForStates(serializer, states, tmpArray);
+                        } catch (IOException e) {
+                            throw new RuntimeException(e);
+                        }
+                    });
+        } catch (RuntimeException e) {
+            if (e.getCause() instanceof IOException) {
+                throw (IOException) e.getCause();
+            } else {
+                throw e;
+            }
+        }
     }
 
-    private void writeXmlAllStates(TypedXmlSerializer serializer, int[] states, int stateIndex,
-            long[] values) throws IOException {
-        if (stateIndex < states.length) {
-            if (!mFactory.mStates[stateIndex].mTracked) {
-                writeXmlAllStates(serializer, states, stateIndex + 1, values);
-                return;
-            }
-
-            for (int i = 0; i < mFactory.mStates[stateIndex].mLabels.length; i++) {
-                states[stateIndex] = i;
-                writeXmlAllStates(serializer, states, stateIndex + 1, values);
-            }
-            return;
-        }
-
+    private void writeXmlForStates(TypedXmlSerializer serializer, int[] states, long[] values)
+            throws IOException {
         mCounter.getCounts(values, mFactory.getSerialState(states));
         boolean nonZero = false;
         for (long value : values) {
@@ -391,48 +445,33 @@
     /**
      * Prints the accumulated stats, one line of every combination of states that has data.
      */
-    public void dump(PrintWriter pw) {
-        long[] tmpArray = new long[mCounter.getArrayLength()];
-        dumpAllStates(pw, new int[mFactory.mStates.length], 0, tmpArray);
-    }
-
-    private void dumpAllStates(PrintWriter pw, int[] states, int stateIndex, long[] values) {
-        if (stateIndex < states.length) {
-            if (!mFactory.mStates[stateIndex].mTracked) {
-                dumpAllStates(pw, states, stateIndex + 1, values);
+    public void dump(PrintWriter pw, Function<long[], String> statsFormatter) {
+        long[] values = new long[mCounter.getArrayLength()];
+        States.forEachTrackedStateCombination(mFactory.mStates, states -> {
+            mCounter.getCounts(values, mFactory.getSerialState(states));
+            boolean nonZero = false;
+            for (long value : values) {
+                if (value != 0) {
+                    nonZero = true;
+                    break;
+                }
+            }
+            if (!nonZero) {
                 return;
             }
 
-            for (int i = 0; i < mFactory.mStates[stateIndex].mLabels.length; i++) {
-                states[stateIndex] = i;
-                dumpAllStates(pw, states, stateIndex + 1, values);
-            }
-            return;
-        }
-
-        mCounter.getCounts(values, mFactory.getSerialState(states));
-        boolean nonZero = false;
-        for (long value : values) {
-            if (value != 0) {
-                nonZero = true;
-                break;
-            }
-        }
-        if (!nonZero) {
-            return;
-        }
-
-        StringBuilder sb = new StringBuilder();
-        for (int i = 0; i < states.length; i++) {
-            if (mFactory.mStates[i].mTracked) {
-                if (sb.length() != 0) {
-                    sb.append(" ");
+            StringBuilder sb = new StringBuilder();
+            for (int i = 0; i < states.length; i++) {
+                if (mFactory.mStates[i].mTracked) {
+                    if (sb.length() != 0) {
+                        sb.append(" ");
+                    }
+                    sb.append(mFactory.mStates[i].mLabels[states[i]]);
                 }
-                sb.append(mFactory.mStates[i].mLabels[states[i]]);
             }
-        }
-        sb.append(" ");
-        sb.append(Arrays.toString(values));
-        pw.println(sb);
+            sb.append(" ");
+            sb.append(statsFormatter.apply(values));
+            pw.println(sb);
+        });
     }
 }
diff --git a/core/java/com/android/internal/os/PowerProfile.java b/core/java/com/android/internal/os/PowerProfile.java
index 503e689..2298cbd 100644
--- a/core/java/com/android/internal/os/PowerProfile.java
+++ b/core/java/com/android/internal/os/PowerProfile.java
@@ -44,7 +44,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
-import java.util.Locale;
 
 /**
  * Reports power consumption values for various device activities. Reads values from an XML file.
@@ -295,7 +294,7 @@
 
     private static final long SUBSYSTEM_FIELDS_MASK = 0xFFFF_FFFF;
 
-    private static final int DEFAULT_CPU_POWER_BRACKET_NUMBER = 3;
+    public static final int POWER_BRACKETS_UNSPECIFIED = -1;
 
     /**
      * A map from Power Use Item to its power consumption.
@@ -361,7 +360,7 @@
         }
         initCpuClusters();
         initCpuScalingPolicies();
-        initCpuPowerBrackets(DEFAULT_CPU_POWER_BRACKET_NUMBER);
+        initCpuPowerBrackets();
         initDisplays();
         initModem();
     }
@@ -560,8 +559,7 @@
     /**
      * Parses or computes CPU power brackets: groups of states with similar power requirements.
      */
-    @VisibleForTesting
-    public void initCpuPowerBrackets(int defaultCpuPowerBracketNumber) {
+    private void initCpuPowerBrackets() {
         boolean anyBracketsSpecified = false;
         boolean allBracketsSpecified = true;
         for (int i = mCpuScalingPolicies.size() - 1; i >= 0; i--) {
@@ -580,79 +578,32 @@
                     "Power brackets should be specified for all scaling policies or none");
         }
 
+        if (!allBracketsSpecified) {
+            mCpuPowerBracketCount = POWER_BRACKETS_UNSPECIFIED;
+            return;
+        }
+
         mCpuPowerBracketCount = 0;
-        if (allBracketsSpecified) {
-            for (int i = mCpuScalingPolicies.size() - 1; i >= 0; i--) {
-                int policy = mCpuScalingPolicies.keyAt(i);
-                CpuScalingPolicyPower cpuScalingPolicyPower = mCpuScalingPolicies.valueAt(i);
-                final Double[] data = sPowerArrayMap.get(CPU_POWER_BRACKETS_PREFIX + policy);
-                if (data.length != cpuScalingPolicyPower.powerBrackets.length) {
-                    throw new RuntimeException(
-                            "Wrong number of items in " + CPU_POWER_BRACKETS_PREFIX + policy
-                                    + ", expected: "
-                                    + cpuScalingPolicyPower.powerBrackets.length);
-                }
-
-                for (int j = 0; j < data.length; j++) {
-                    final int bracket = (int) Math.round(data[j]);
-                    cpuScalingPolicyPower.powerBrackets[j] = bracket;
-                    if (bracket > mCpuPowerBracketCount) {
-                        mCpuPowerBracketCount = bracket;
-                    }
-                }
-            }
-            mCpuPowerBracketCount++;
-        } else {
-            double minPower = Double.MAX_VALUE;
-            double maxPower = Double.MIN_VALUE;
-            int stateCount = 0;
-            for (int i = mCpuScalingPolicies.size() - 1; i >= 0; i--) {
-                int policy = mCpuScalingPolicies.keyAt(i);
-                CpuScalingPolicyPower cpuScalingPolicyPower = mCpuScalingPolicies.valueAt(i);
-                final int steps = cpuScalingPolicyPower.stepPower.length;
-                for (int step = 0; step < steps; step++) {
-                    final double power = getAveragePowerForCpuScalingStep(policy, step);
-                    if (power < minPower) {
-                        minPower = power;
-                    }
-                    if (power > maxPower) {
-                        maxPower = power;
-                    }
-                }
-                stateCount += steps;
+        for (int i = mCpuScalingPolicies.size() - 1; i >= 0; i--) {
+            int policy = mCpuScalingPolicies.keyAt(i);
+            CpuScalingPolicyPower cpuScalingPolicyPower = mCpuScalingPolicies.valueAt(i);
+            final Double[] data = sPowerArrayMap.get(CPU_POWER_BRACKETS_PREFIX + policy);
+            if (data.length != cpuScalingPolicyPower.powerBrackets.length) {
+                throw new RuntimeException(
+                        "Wrong number of items in " + CPU_POWER_BRACKETS_PREFIX + policy
+                                + ", expected: "
+                                + cpuScalingPolicyPower.powerBrackets.length);
             }
 
-            if (stateCount <= defaultCpuPowerBracketNumber) {
-                mCpuPowerBracketCount = stateCount;
-                int bracket = 0;
-                for (int i = 0; i < mCpuScalingPolicies.size(); i++) {
-                    CpuScalingPolicyPower cpuScalingPolicyPower = mCpuScalingPolicies.valueAt(i);
-                    final int steps = cpuScalingPolicyPower.stepPower.length;
-                    for (int step = 0; step < steps; step++) {
-                        cpuScalingPolicyPower.powerBrackets[step] = bracket++;
-                    }
-                }
-            } else {
-                mCpuPowerBracketCount = defaultCpuPowerBracketNumber;
-                final double minLogPower = Math.log(minPower);
-                final double logBracket = (Math.log(maxPower) - minLogPower)
-                        / defaultCpuPowerBracketNumber;
-
-                for (int i = mCpuScalingPolicies.size() - 1; i >= 0; i--) {
-                    int policy = mCpuScalingPolicies.keyAt(i);
-                    CpuScalingPolicyPower cpuScalingPolicyPower = mCpuScalingPolicies.valueAt(i);
-                    final int steps = cpuScalingPolicyPower.stepPower.length;
-                    for (int step = 0; step < steps; step++) {
-                        final double power = getAveragePowerForCpuScalingStep(policy, step);
-                        int bracket = (int) ((Math.log(power) - minLogPower) / logBracket);
-                        if (bracket >= defaultCpuPowerBracketNumber) {
-                            bracket = defaultCpuPowerBracketNumber - 1;
-                        }
-                        cpuScalingPolicyPower.powerBrackets[step] = bracket;
-                    }
+            for (int j = 0; j < data.length; j++) {
+                final int bracket = (int) Math.round(data[j]);
+                cpuScalingPolicyPower.powerBrackets[j] = bracket;
+                if (bracket > mCpuPowerBracketCount) {
+                    mCpuPowerBracketCount = bracket;
                 }
             }
         }
+        mCpuPowerBracketCount++;
     }
 
     private static class CpuScalingPolicyPower {
@@ -771,44 +722,13 @@
 
     /**
      * Returns the number of CPU power brackets: groups of states with similar power requirements.
+     * If power brackets are not specified, returns {@link #POWER_BRACKETS_UNSPECIFIED}
      */
     public int getCpuPowerBracketCount() {
         return mCpuPowerBracketCount;
     }
 
     /**
-     * Description of a CPU power bracket: which cluster/frequency combinations are included.
-     */
-    public String getCpuPowerBracketDescription(CpuScalingPolicies cpuScalingPolicies,
-            int powerBracket) {
-        StringBuilder sb = new StringBuilder();
-        for (int i = 0; i < mCpuScalingPolicies.size(); i++) {
-            int policy = mCpuScalingPolicies.keyAt(i);
-            CpuScalingPolicyPower cpuScalingPolicyPower = mCpuScalingPolicies.valueAt(i);
-            int[] brackets = cpuScalingPolicyPower.powerBrackets;
-            int[] freqs = cpuScalingPolicies.getFrequencies(policy);
-            for (int step = 0; step < brackets.length; step++) {
-                if (brackets[step] == powerBracket) {
-                    if (sb.length() != 0) {
-                        sb.append(", ");
-                    }
-                    if (mCpuScalingPolicies.size() > 1) {
-                        sb.append(policy).append('/');
-                    }
-                    if (step < freqs.length) {
-                        sb.append(freqs[step] / 1000);
-                    }
-                    sb.append('(');
-                    sb.append(String.format(Locale.US, "%.1f",
-                            getAveragePowerForCpuScalingStep(policy, step)));
-                    sb.append(')');
-                }
-            }
-        }
-        return sb.toString();
-    }
-
-    /**
      * Returns the CPU power bracket corresponding to the specified scaling policy and frequency
      * step
      */
diff --git a/core/java/com/android/internal/os/PowerStats.java b/core/java/com/android/internal/os/PowerStats.java
index 1130a45..1a7efac 100644
--- a/core/java/com/android/internal/os/PowerStats.java
+++ b/core/java/com/android/internal/os/PowerStats.java
@@ -55,12 +55,12 @@
     private static final int STATS_ARRAY_LENGTH_SHIFT =
             Integer.numberOfTrailingZeros(STATS_ARRAY_LENGTH_MASK);
     public static final int MAX_STATS_ARRAY_LENGTH =
-            2 ^ Integer.bitCount(STATS_ARRAY_LENGTH_MASK) - 1;
+            (1 << Integer.bitCount(STATS_ARRAY_LENGTH_MASK)) - 1;
     private static final int UID_STATS_ARRAY_LENGTH_MASK = 0x00FF0000;
     private static final int UID_STATS_ARRAY_LENGTH_SHIFT =
             Integer.numberOfTrailingZeros(UID_STATS_ARRAY_LENGTH_MASK);
     public static final int MAX_UID_STATS_ARRAY_LENGTH =
-            (2 ^ Integer.bitCount(UID_STATS_ARRAY_LENGTH_MASK)) - 1;
+            (1 << Integer.bitCount(UID_STATS_ARRAY_LENGTH_MASK)) - 1;
 
     /**
      * Descriptor of the stats collected for a given power component (e.g. CPU, WiFi etc).
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index a3e2706..8d11672 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -1934,15 +1934,21 @@
     }
 
     /**
-     * Unlocks the credential-encrypted storage for the given user if the user is not secured, i.e.
-     * doesn't have an LSKF.
+     * If the user is not secured, ie doesn't have an LSKF, then decrypt the user's synthetic
+     * password and use it to unlock various cryptographic keys associated with the user.  This
+     * primarily includes unlocking the user's credential-encrypted (CE) storage.  It also includes
+     * deriving or decrypting the vendor auth secret and sending it to the AuthSecret HAL.
      * <p>
-     * Whether the storage has been unlocked can be determined by
-     * {@link StorageManager#isUserKeyUnlocked()}.
-     *
+     * These tasks would normally be done when the LSKF is verified.  This method is where these
+     * tasks are done when the user doesn't have an LSKF.  It's called when the user is started.
+     * <p>
+     * Except on permission denied, this method doesn't throw an exception on failure.  However, the
+     * last thing that it does is unlock CE storage, and whether CE storage has been successfully
+     * unlocked can be determined by {@link StorageManager#isCeStorageUnlocked()}.
+     * <p>
      * Requires the {@link android.Manifest.permission#ACCESS_KEYGUARD_SECURE_STORAGE} permission.
      *
-     * @param userId the ID of the user whose storage to unlock
+     * @param userId the ID of the user whose keys to unlock
      */
     public void unlockUserKeyIfUnsecured(@UserIdInt int userId) {
         try {
diff --git a/core/proto/android/app/appstartinfo.proto b/core/proto/android/app/appstartinfo.proto
new file mode 100644
index 0000000..8c33041
--- /dev/null
+++ b/core/proto/android/app/appstartinfo.proto
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+syntax = "proto2";
+option java_multiple_files = true;
+
+package android.app;
+
+import "frameworks/base/core/proto/android/privacy.proto";
+import "frameworks/proto_logging/stats/enums/app/enums.proto";
+
+/**
+ * An android.app.ApplicationStartInfo object.
+ */
+message ApplicationStartInfoProto {
+    option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
+    optional int32 pid = 1;
+    optional int32 real_uid = 2;
+    optional int32 package_uid = 3;
+    optional int32 defining_uid = 4;
+    optional string process_name = 5;
+    optional AppStartStartupState startup_state = 6;
+    optional AppStartReasonCode reason = 7;
+    optional bytes startup_timestamps = 8;
+    optional AppStartStartType start_type = 9;
+    optional bytes start_intent = 10;
+    optional AppStartLaunchMode launch_mode = 11;
+}
diff --git a/core/proto/android/server/activitymanagerservice.proto b/core/proto/android/server/activitymanagerservice.proto
index 025a57d..c5889ba 100644
--- a/core/proto/android/server/activitymanagerservice.proto
+++ b/core/proto/android/server/activitymanagerservice.proto
@@ -20,6 +20,7 @@
 
 import "frameworks/base/core/proto/android/app/activitymanager.proto";
 import "frameworks/base/core/proto/android/app/appexitinfo.proto";
+import "frameworks/base/core/proto/android/app/appstartinfo.proto";
 import "frameworks/base/core/proto/android/app/notification.proto";
 import "frameworks/base/core/proto/android/app/profilerinfo.proto";
 import "frameworks/base/core/proto/android/content/component_name.proto";
@@ -1041,3 +1042,23 @@
     }
     repeated Package packages = 2;
 }
+
+// sync with com.android.server.am.am.ProcessList.java
+message AppsStartInfoProto {
+    option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
+    optional int64 last_update_timestamp = 1;
+    message Package {
+        option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
+        optional string package_name = 1;
+        message User {
+            option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
+            optional int32 uid = 1;
+            repeated .android.app.ApplicationStartInfoProto app_start_info = 2;
+        }
+        repeated User users = 2;
+    }
+    repeated Package packages = 2;
+}
diff --git a/core/proto/android/service/OWNERS b/core/proto/android/service/OWNERS
index 70cb50f..7a19155 100644
--- a/core/proto/android/service/OWNERS
+++ b/core/proto/android/service/OWNERS
@@ -1 +1,2 @@
 per-file sensor_service.proto = arthuri@google.com, bduddie@google.com, stange@google.com
+per-file package.proto = file:platform/frameworks/base:/PACKAGE_MANAGER_OWNERS
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 0a81209..8c91be8 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -4653,7 +4653,7 @@
         @hide
         @SystemApi -->
     <permission android:name="android.permission.STATUS_BAR_SERVICE"
-        android:protectionLevel="signature" />
+        android:protectionLevel="signature|recents" />
 
     <!-- Allows an application to bind to third party quick settings tiles.
          <p>Should only be requested by the System, should be required by
@@ -4717,7 +4717,7 @@
          @hide
     -->
     <permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW"
-        android:protectionLevel="signature|module" />
+        android:protectionLevel="signature|module|recents" />
 
     <!-- Allows an application to avoid all toast rate limiting restrictions.
          <p>Not for use by third-party applications.
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 5a1f2d1a..ab71b41 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1241,6 +1241,21 @@
    <!-- Activity name for the default target activity to be launched. [DO NOT TRANSLATE] -->
    <string name="config_primaryShortPressTargetActivity" translatable="false"></string>
 
+    <!-- Whether a single short press on POWER should be launched without multi-press delay.
+        When this value is set to true, POWER button's single short press behavior will execute
+        immediately upon key-up regardless of whether this press will be part of a multi-press
+        gesture in the future(therefore, not waiting for a multi-press detecting delay).
+
+       For Example, let's say a power button single press launches the app menu and power button
+       double press launches the camera. By configuring this variable to true, user will observe 2
+       things in order upon a double press:
+          1. App menu pops up immediately upon the first key up.
+          2. Camera starts as the double press behavior.
+
+        Note that this config has no effect on long press behavior.
+    -->
+    <bool name="config_shortPressEarlyOnPower">false</bool>
+
     <!-- Control the behavior of the search key.
             0 - Launch default search activity
             1 - Launch target activity defined by config_searchKeyTargetActivity
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 8e1c09e..14bbb96 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -471,6 +471,7 @@
   <java-symbol type="string" name="config_primaryShortPressTargetActivity" />
   <java-symbol type="integer" name="config_doublePressOnStemPrimaryBehavior" />
   <java-symbol type="integer" name="config_triplePressOnStemPrimaryBehavior" />
+  <java-symbol type="bool" name="config_shortPressEarlyOnPower" />
   <java-symbol type="string" name="config_doublePressOnPowerTargetActivity" />
   <java-symbol type="integer" name="config_searchKeyBehavior" />
   <java-symbol type="string" name="config_searchKeyTargetActivity" />
diff --git a/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java b/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java
index 72969f7..37a499a 100644
--- a/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java
+++ b/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java
@@ -104,19 +104,15 @@
     private static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT";
 
     private static final Path[] UI_TRACES_PREDUMPED = {
+            Paths.get("/data/misc/perfetto-traces/bugreport/systrace.pftrace"),
             Paths.get("/data/misc/wmtrace/ime_trace_clients.winscope"),
             Paths.get("/data/misc/wmtrace/ime_trace_managerservice.winscope"),
             Paths.get("/data/misc/wmtrace/ime_trace_service.winscope"),
             Paths.get("/data/misc/wmtrace/wm_trace.winscope"),
             Paths.get("/data/misc/wmtrace/wm_log.winscope"),
-            Paths.get("/data/misc/wmtrace/layers_trace.winscope"),
-            Paths.get("/data/misc/wmtrace/transactions_trace.winscope"),
-            Paths.get("/data/misc/wmtrace/transition_trace.winscope"),
+            Paths.get("/data/misc/wmtrace/wm_transition_trace.winscope"),
             Paths.get("/data/misc/wmtrace/shell_transition_trace.winscope"),
     };
-    private static final Path[] UI_TRACES_GENERATED_DURING_BUGREPORT = {
-            Paths.get("/data/misc/wmtrace/layers_trace_from_transactions.winscope"),
-    };
 
     private Handler mHandler;
     private Executor mExecutor;
@@ -210,7 +206,7 @@
 
         mBrm.preDumpUiData();
         waitTillDumpstateExitedOrTimeout();
-        List<File> expectedPreDumpedTraceFiles = copyFilesAsRoot(UI_TRACES_PREDUMPED);
+        List<File> expectedPreDumpedTraceFiles = copyFiles(UI_TRACES_PREDUMPED);
 
         BugreportCallbackImpl callback = new BugreportCallbackImpl();
         mBrm.startBugreport(mBugreportFd, null, fullWithUsePreDumpFlag(), mExecutor,
@@ -225,7 +221,6 @@
         assertFdsAreClosed(mBugreportFd);
 
         assertThatBugreportContainsFiles(UI_TRACES_PREDUMPED);
-        assertThatBugreportContainsFiles(UI_TRACES_GENERATED_DURING_BUGREPORT);
 
         List<File> actualPreDumpedTraceFiles = extractFilesFromBugreport(UI_TRACES_PREDUMPED);
         assertThatAllFileContentsAreEqual(actualPreDumpedTraceFiles, expectedPreDumpedTraceFiles);
@@ -240,9 +235,9 @@
         // In some corner cases, data dumped as part of the full bugreport could be the same as the
         // pre-dumped data and this test would fail. Hence, here we create fake/artificial
         // pre-dumped data that we know it won't match with the full bugreport data.
-        createFilesWithFakeDataAsRoot(UI_TRACES_PREDUMPED, "system");
+        createFakeTraceFiles(UI_TRACES_PREDUMPED);
 
-        List<File> preDumpedTraceFiles = copyFilesAsRoot(UI_TRACES_PREDUMPED);
+        List<File> preDumpedTraceFiles = copyFiles(UI_TRACES_PREDUMPED);
 
         BugreportCallbackImpl callback = new BugreportCallbackImpl();
         mBrm.startBugreport(mBugreportFd, null, full(), mExecutor,
@@ -257,7 +252,6 @@
         assertFdsAreClosed(mBugreportFd);
 
         assertThatBugreportContainsFiles(UI_TRACES_PREDUMPED);
-        assertThatBugreportContainsFiles(UI_TRACES_GENERATED_DURING_BUGREPORT);
 
         List<File> actualTraceFiles = extractFilesFromBugreport(UI_TRACES_PREDUMPED);
         assertThatAllFileContentsAreDifferent(preDumpedTraceFiles, actualTraceFiles);
@@ -480,7 +474,32 @@
         return f;
     }
 
-    private static void startPreDumpedUiTraces() {
+    private static void startPreDumpedUiTraces() throws Exception {
+        // Perfetto traces
+        String perfettoConfig =
+                "buffers: {\n"
+                + "    size_kb: 2048\n"
+                + "    fill_policy: RING_BUFFER\n"
+                + "}\n"
+                + "data_sources: {\n"
+                + "    config {\n"
+                + "        name: \"android.surfaceflinger.transactions\"\n"
+                + "    }\n"
+                + "}\n"
+                + "bugreport_score: 10\n";
+        File tmp = createTempFile("tmp", ".cfg");
+        Files.write(perfettoConfig.getBytes(StandardCharsets.UTF_8), tmp);
+        InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
+                "install -m 644 -o root -g root "
+                + tmp.getAbsolutePath() + " /data/misc/perfetto-configs/bugreport-manager-test.cfg"
+        );
+        InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
+                "perfetto --background-wait"
+                + " --config /data/misc/perfetto-configs/bugreport-manager-test.cfg --txt"
+                + " --out /data/misc/perfetto-traces/not-used.perfetto-trace"
+        );
+
+        // Legacy traces
         InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
                 "cmd input_method tracing start"
         );
@@ -563,19 +582,24 @@
         return extractedFile;
     }
 
-    private static void createFilesWithFakeDataAsRoot(Path[] paths, String owner) throws Exception {
+    private static void createFakeTraceFiles(Path[] paths) throws Exception {
         File src = createTempFile("fake", ".data");
         Files.write("fake data".getBytes(StandardCharsets.UTF_8), src);
 
         for (Path path : paths) {
             InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
-                    "install -m 611 -o " + owner + " -g " + owner
-                    + " " + src.getAbsolutePath() + " " + path.toString()
+                    "install -m 644 -o system -g system "
+                    + src.getAbsolutePath() + " " + path.toString()
             );
         }
+
+        // Dumpstate executes "perfetto --save-for-bugreport" as shell
+        InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
+                "chown shell:shell /data/misc/perfetto-traces/bugreport/systrace.pftrace"
+        );
     }
 
-    private static List<File> copyFilesAsRoot(Path[] paths) throws Exception {
+    private static List<File> copyFiles(Path[] paths) throws Exception {
         ArrayList<File> files = new ArrayList<File>();
         for (Path src : paths) {
             File dst = createTempFile(src.getFileName().toString(), ".copy");
diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java
index 0f62b1c..930b1a4 100644
--- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java
+++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java
@@ -16,14 +16,19 @@
 
 package android.app.servertransaction;
 
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.clearInvocations;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
 import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManagerGlobal;
+import android.hardware.display.IDisplayManager;
+import android.os.Handler;
+import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
+import android.view.DisplayInfo;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -34,8 +39,6 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.util.function.IntConsumer;
-
 /**
  * Tests for {@link ClientTransactionListenerController}.
  *
@@ -47,30 +50,36 @@
 @Presubmit
 public class ClientTransactionListenerControllerTest {
     @Mock
-    private IntConsumer mDisplayChangeListener;
+    private IDisplayManager mIDisplayManager;
+    @Mock
+    private DisplayManager.DisplayListener mListener;
 
+    private DisplayManagerGlobal mDisplayManager;
+    private Handler mHandler;
     private ClientTransactionListenerController mController;
 
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-        mController = spy(ClientTransactionListenerController.createInstanceForTesting());
+        mDisplayManager = new DisplayManagerGlobal(mIDisplayManager);
+        mHandler = getInstrumentation().getContext().getMainThreadHandler();
+        mController = spy(ClientTransactionListenerController.createInstanceForTesting(
+                mDisplayManager));
         doReturn(true).when(mController).isSyncWindowConfigUpdateFlagEnabled();
     }
 
     @Test
-    public void testRegisterDisplayChangeListener() {
-        mController.registerDisplayChangeListener(mDisplayChangeListener, Runnable::run);
+    public void testOnDisplayChanged() throws RemoteException {
+        // Mock IDisplayManager to return a display info to trigger display change.
+        final DisplayInfo newDisplayInfo = new DisplayInfo();
+        doReturn(newDisplayInfo).when(mIDisplayManager).getDisplayInfo(123);
+
+        mDisplayManager.registerDisplayListener(mListener, mHandler,
+                DisplayManager.EVENT_FLAG_DISPLAY_CHANGED, null /* packageName */);
 
         mController.onDisplayChanged(123);
+        mHandler.runWithScissors(() -> { }, 0);
 
-        verify(mDisplayChangeListener).accept(123);
-
-        clearInvocations(mDisplayChangeListener);
-        mController.unregisterDisplayChangeListener(mDisplayChangeListener);
-
-        mController.onDisplayChanged(321);
-
-        verify(mDisplayChangeListener, never()).accept(anyInt());
+        verify(mListener).onDisplayChanged(123);
     }
 }
diff --git a/core/tests/coretests/src/android/app/usage/ParcelableUsageEventListTest.java b/core/tests/coretests/src/android/app/usage/ParcelableUsageEventListTest.java
new file mode 100644
index 0000000..2ec58d4
--- /dev/null
+++ b/core/tests/coretests/src/android/app/usage/ParcelableUsageEventListTest.java
@@ -0,0 +1,164 @@
+/*
+ * 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.app.usage;
+
+import static android.view.Surface.ROTATION_90;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+
+import android.app.usage.UsageEvents.Event;
+import android.content.res.Configuration;
+import android.os.Parcel;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class ParcelableUsageEventListTest {
+    private static final int SMALL_TEST_EVENT_COUNT = 100;
+    private static final int LARGE_TEST_EVENT_COUNT = 10000;
+
+    private Random mRandom = new Random();
+
+    @Test
+    public void testSmallList() throws Exception {
+        testParcelableUsageEventList(SMALL_TEST_EVENT_COUNT);
+    }
+
+    @Test
+    public void testLargeList() throws Exception {
+        testParcelableUsageEventList(LARGE_TEST_EVENT_COUNT);
+    }
+
+    private void testParcelableUsageEventList(int eventCount) throws Exception {
+        List<Event> smallList = new ArrayList<>();
+        for (int i = 0; i < eventCount; i++) {
+            smallList.add(generateUsageEvent());
+        }
+
+        ParcelableUsageEventList slice;
+        Parcel parcel = Parcel.obtain();
+        try {
+            parcel.writeParcelable(new ParcelableUsageEventList(smallList), 0);
+            parcel.setDataPosition(0);
+            slice = parcel.readParcelable(getClass().getClassLoader(),
+                    ParcelableUsageEventList.class);
+        } finally {
+            parcel.recycle();
+        }
+
+        assertNotNull(slice);
+        assertNotNull(slice.getList());
+        assertEquals(eventCount, slice.getList().size());
+
+        for (int i = 0; i < eventCount; i++) {
+            compareUsageEvent(smallList.get(i), slice.getList().get(i));
+        }
+    }
+
+    private Event generateUsageEvent() {
+        final Event event = new Event();
+        event.mEventType = mRandom.nextInt(Event.MAX_EVENT_TYPE + 1);
+        event.mPackage = anyString();
+        event.mClass = anyString();
+        event.mTimeStamp = anyLong();
+        event.mInstanceId = anyInt();
+        event.mTimeStamp = anyLong();
+
+        switch (event.mEventType) {
+            case Event.CONFIGURATION_CHANGE:
+                event.mConfiguration = new Configuration();
+                event.mConfiguration.seq = anyInt();
+                event.mConfiguration.screenLayout = Configuration.SCREENLAYOUT_ROUND_YES;
+                event.mConfiguration.smallestScreenWidthDp = 100;
+                event.mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE;
+                event.mConfiguration.windowConfiguration.setRotation(ROTATION_90);
+                break;
+            case Event.SHORTCUT_INVOCATION:
+                event.mShortcutId = anyString();
+                break;
+            case Event.CHOOSER_ACTION:
+                event.mAction = anyString();
+                event.mContentType = anyString();
+                event.mContentAnnotations = new String[mRandom.nextInt(10)];
+                for (int i = 0; i < event.mContentAnnotations.length; i++) {
+                    event.mContentAnnotations[i] = anyString();
+                }
+                break;
+            case Event.STANDBY_BUCKET_CHANGED:
+                event.mBucketAndReason = anyInt();
+                break;
+            case Event.NOTIFICATION_INTERRUPTION:
+                event.mNotificationChannelId = anyString();
+                break;
+            case Event.LOCUS_ID_SET:
+                event.mLocusId = anyString();
+                break;
+        }
+
+        event.mFlags = anyInt();
+        return event;
+    }
+
+    private static void compareUsageEvent(Event ue1, Event ue2) {
+        assertEquals(ue1.mPackage, ue2.mPackage);
+        assertEquals(ue1.mClass, ue2.mClass);
+        assertEquals(ue1.mTaskRootPackage, ue2.mTaskRootPackage);
+        assertEquals(ue1.mTaskRootClass, ue2.mTaskRootClass);
+        assertEquals(ue1.mInstanceId, ue2.mInstanceId);
+        assertEquals(ue1.mEventType, ue2.mEventType);
+        assertEquals(ue1.mTimeStamp, ue2.mTimeStamp);
+
+        switch (ue1.mEventType) {
+            case Event.CONFIGURATION_CHANGE:
+                assertEquals(ue1.mConfiguration, ue2.mConfiguration);
+                break;
+            case Event.SHORTCUT_INVOCATION:
+                assertEquals(ue1.mShortcutId, ue2.mShortcutId);
+                break;
+            case Event.CHOOSER_ACTION:
+                assertEquals(ue1.mAction, ue2.mAction);
+                assertEquals(ue1.mContentType, ue2.mContentType);
+                assertTrue(Arrays.equals(ue1.mContentAnnotations, ue2.mContentAnnotations));
+                break;
+            case Event.STANDBY_BUCKET_CHANGED:
+                assertEquals(ue1.mBucketAndReason, ue2.mBucketAndReason);
+                break;
+            case Event.NOTIFICATION_INTERRUPTION:
+                assertEquals(ue1.mNotificationChannelId, ue1.mNotificationChannelId);
+                break;
+            case Event.LOCUS_ID_SET:
+                assertEquals(ue1.mLocusId, ue2.mLocusId);
+                break;
+        }
+
+        assertEquals(ue1.mFlags, ue2.mFlags);
+    }
+}
diff --git a/core/tests/coretests/src/android/content/BrickDeniedTest.java b/core/tests/coretests/src/android/content/BrickDeniedTest.java
deleted file mode 100644
index d8c9baa..0000000
--- a/core/tests/coretests/src/android/content/BrickDeniedTest.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2008 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.content;
-
-import android.test.AndroidTestCase;
-
-import androidx.test.filters.SmallTest;
-
-/** Test to make sure brick intents <b>don't</b> work without permission. */
-public class BrickDeniedTest extends AndroidTestCase {
-    @SmallTest
-    public void testBrick() {
-        // Try both the old and new brick intent names.  Neither should work,
-        // since this test application doesn't have the required permission.
-        // If it does work, well, the test certainly won't pass.
-        getContext().sendBroadcast(new Intent("SHES_A_BRICK_HOUSE"));
-        getContext().sendBroadcast(new Intent("android.intent.action.BRICK"));
-    }
-}
diff --git a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
index c2e6b60c..969ae8e 100644
--- a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
+++ b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
@@ -16,12 +16,19 @@
 
 package android.hardware.display;
 
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
 
 import android.content.Context;
 import android.os.Handler;
 import android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
+import android.view.DisplayInfo;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -37,6 +44,13 @@
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
+/**
+ * Tests for {@link DisplayManagerGlobal}.
+ *
+ * Build/Install/Run:
+ *  atest FrameworksCoreTests:DisplayManagerGlobalTest
+ */
+@Presubmit
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class DisplayManagerGlobalTest {
@@ -51,6 +65,9 @@
     @Mock
     private DisplayManager.DisplayListener mListener;
 
+    @Mock
+    private DisplayManager.DisplayListener mListener2;
+
     @Captor
     private ArgumentCaptor<IDisplayManagerCallback> mCallbackCaptor;
 
@@ -82,7 +99,11 @@
         Mockito.verifyNoMoreInteractions(mListener);
 
         Mockito.reset(mListener);
-        callback.onDisplayEvent(1, DisplayManagerGlobal.EVENT_DISPLAY_CHANGED);
+        // Mock IDisplayManager to return a different display info to trigger display change.
+        final DisplayInfo newDisplayInfo = new DisplayInfo();
+        newDisplayInfo.rotation++;
+        doReturn(newDisplayInfo).when(mDisplayManager).getDisplayInfo(displayId);
+        callback.onDisplayEvent(displayId, DisplayManagerGlobal.EVENT_DISPLAY_CHANGED);
         waitForHandler();
         Mockito.verify(mListener).onDisplayChanged(eq(displayId));
         Mockito.verifyNoMoreInteractions(mListener);
@@ -161,7 +182,44 @@
         mDisplayManagerGlobal.unregisterDisplayListener(mListener);
         inOrder.verify(mDisplayManager)
                 .registerCallbackWithEventMask(mCallbackCaptor.capture(), eq(0L));
+    }
 
+    @Test
+    public void testHandleDisplayChangeFromWindowManager() throws RemoteException {
+        // Mock IDisplayManager to return a display info to trigger display change.
+        final DisplayInfo newDisplayInfo = new DisplayInfo();
+        doReturn(newDisplayInfo).when(mDisplayManager).getDisplayInfo(123);
+        doReturn(newDisplayInfo).when(mDisplayManager).getDisplayInfo(321);
+
+        // Nothing happens when there is no listener.
+        mDisplayManagerGlobal.handleDisplayChangeFromWindowManager(123);
+
+        // One listener listens on add/remove, and the other one listens on change.
+        mDisplayManagerGlobal.registerDisplayListener(mListener, mHandler,
+                DisplayManager.EVENT_FLAG_DISPLAY_ADDED
+                        | DisplayManager.EVENT_FLAG_DISPLAY_REMOVED, null /* packageName */);
+        mDisplayManagerGlobal.registerDisplayListener(mListener2, mHandler,
+                DisplayManager.EVENT_FLAG_DISPLAY_CHANGED, null /* packageName */);
+
+        mDisplayManagerGlobal.handleDisplayChangeFromWindowManager(321);
+        waitForHandler();
+
+        verify(mListener, never()).onDisplayChanged(anyInt());
+        verify(mListener2).onDisplayChanged(321);
+
+        // Trigger the callback again even if the display info is not changed.
+        clearInvocations(mListener2);
+        mDisplayManagerGlobal.handleDisplayChangeFromWindowManager(321);
+        waitForHandler();
+
+        verify(mListener2).onDisplayChanged(321);
+
+        // No callback for non-existing display (no display info returned from IDisplayManager).
+        clearInvocations(mListener2);
+        mDisplayManagerGlobal.handleDisplayChangeFromWindowManager(456);
+        waitForHandler();
+
+        verify(mListener2, never()).onDisplayChanged(anyInt());
     }
 
     private void waitForHandler() {
diff --git a/core/tests/coretests/src/com/android/internal/os/PowerProfileTest.java b/core/tests/coretests/src/com/android/internal/os/PowerProfileTest.java
index 8fa6376..77202d1 100644
--- a/core/tests/coretests/src/com/android/internal/os/PowerProfileTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/PowerProfileTest.java
@@ -21,16 +21,12 @@
 import static com.android.internal.os.PowerProfile.POWER_GROUP_DISPLAY_SCREEN_FULL;
 import static com.android.internal.os.PowerProfile.POWER_GROUP_DISPLAY_SCREEN_ON;
 
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
 
 import android.annotation.XmlRes;
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.XmlResourceParser;
-import android.util.SparseArray;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -540,66 +536,4 @@
     private void assertEquals(double expected, double actual) {
         Assert.assertEquals(expected, actual, 0.1);
     }
-
-    @Test
-    public void powerBrackets_specifiedInPowerProfile() {
-        mProfile.forceInitForTesting(mContext, R.xml.power_profile_test_power_brackets);
-        mProfile.initCpuPowerBrackets(8);
-
-        int cpuPowerBracketCount = mProfile.getCpuPowerBracketCount();
-        assertThat(cpuPowerBracketCount).isEqualTo(2);
-        assertThat(new int[]{
-                mProfile.getCpuPowerBracketForScalingStep(0, 0),
-                mProfile.getCpuPowerBracketForScalingStep(4, 0),
-                mProfile.getCpuPowerBracketForScalingStep(4, 1),
-        }).isEqualTo(new int[]{1, 1, 0});
-    }
-
-    @Test
-    public void powerBrackets_automatic() {
-        mProfile.forceInitForTesting(mContext, R.xml.power_profile_test);
-        CpuScalingPolicies scalingPolicies = new CpuScalingPolicies(
-                new SparseArray<>() {{
-                    put(0, new int[]{0, 1, 2});
-                    put(3, new int[]{3, 4});
-                }},
-                new SparseArray<>() {{
-                    put(0, new int[]{300000, 1000000, 2000000});
-                    put(3, new int[]{300000, 1000000, 2500000, 3000000});
-                }});
-
-        assertThat(mProfile.getCpuPowerBracketCount()).isEqualTo(3);
-        assertThat(mProfile.getCpuPowerBracketDescription(scalingPolicies, 0))
-                .isEqualTo("0/300(10.0)");
-        assertThat(mProfile.getCpuPowerBracketDescription(scalingPolicies, 1))
-                .isEqualTo("0/1000(20.0), 0/2000(30.0), 3/300(25.0)");
-        assertThat(mProfile.getCpuPowerBracketDescription(scalingPolicies, 2))
-                .isEqualTo("3/1000(35.0), 3/2500(50.0), 3/3000(60.0)");
-        assertThat(new int[]{
-                mProfile.getCpuPowerBracketForScalingStep(0, 0),
-                mProfile.getCpuPowerBracketForScalingStep(0, 1),
-                mProfile.getCpuPowerBracketForScalingStep(0, 2),
-                mProfile.getCpuPowerBracketForScalingStep(3, 0),
-                mProfile.getCpuPowerBracketForScalingStep(3, 1),
-                mProfile.getCpuPowerBracketForScalingStep(3, 2),
-                mProfile.getCpuPowerBracketForScalingStep(3, 3),
-        }).isEqualTo(new int[]{0, 1, 1, 1, 2, 2, 2});
-    }
-
-    @Test
-    public void powerBrackets_moreBracketsThanStates() {
-        mProfile.forceInitForTesting(mContext, R.xml.power_profile_test);
-        mProfile.initCpuPowerBrackets(8);
-
-        assertThat(mProfile.getCpuPowerBracketCount()).isEqualTo(7);
-        assertThat(new int[]{
-                mProfile.getCpuPowerBracketForScalingStep(0, 0),
-                mProfile.getCpuPowerBracketForScalingStep(0, 1),
-                mProfile.getCpuPowerBracketForScalingStep(0, 2),
-                mProfile.getCpuPowerBracketForScalingStep(3, 0),
-                mProfile.getCpuPowerBracketForScalingStep(3, 1),
-                mProfile.getCpuPowerBracketForScalingStep(3, 2),
-                mProfile.getCpuPowerBracketForScalingStep(3, 3),
-        }).isEqualTo(new int[]{0, 1, 2, 3, 4, 5, 6});
-    }
 }
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index cc73ece..35498b7 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -2365,6 +2365,12 @@
       "group": "WM_DEBUG_RECENTS_ANIMATIONS",
       "at": "com\/android\/server\/wm\/RecentsAnimationController.java"
     },
+    "33989965": {
+      "message": " Met condition %s for #%d (%d left)",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WINDOW_TRANSITIONS",
+      "at": "com\/android\/server\/wm\/Transition.java"
+    },
     "34106798": {
       "message": "Content Recording: Display %d state was (%d), is now (%d), so update recording?",
       "level": "VERBOSE",
@@ -4483,6 +4489,12 @@
       "group": "WM_DEBUG_APP_TRANSITIONS",
       "at": "com\/android\/server\/wm\/AppTransitionController.java"
     },
+    "2053743391": {
+      "message": " Add condition %s for #%d",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WINDOW_TRANSITIONS",
+      "at": "com\/android\/server\/wm\/Transition.java"
+    },
     "2060978050": {
       "message": "moveWindowTokenToDisplay: Attempted to move token: %s to non-exiting displayId=%d",
       "level": "WARN",
@@ -4543,6 +4555,12 @@
       "group": "WM_DEBUG_IME",
       "at": "com\/android\/server\/wm\/DisplayContent.java"
     },
+    "2124732293": {
+      "message": "#%d: Met condition: %s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WINDOW_TRANSITIONS",
+      "at": "com\/android\/server\/wm\/Transition.java"
+    },
     "2128917433": {
       "message": "onProposedRotationChanged, rotation=%d",
       "level": "VERBOSE",
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 49606f0..7743ad5 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -1763,6 +1763,15 @@
             return;
         }
 
+        if (container.isFinished()) {
+            return;
+        }
+
+        if (container.isOverlay()) {
+            updateOverlayContainer(wct, container);
+            return;
+        }
+
         if (launchPlaceholderIfNecessary(wct, container)) {
             // Placeholder was launched, the positions will be updated when the activity is added
             // to the secondary container.
@@ -1783,6 +1792,25 @@
         updateSplitContainerIfNeeded(splitContainer, wct, null /* splitAttributes */);
     }
 
+
+    @VisibleForTesting
+    // Suppress GuardedBy warning because lint ask to mark this method as
+    // @GuardedBy(mPresenter.mController.mLock), which is mLock itself
+    @SuppressWarnings("GuardedBy")
+    @GuardedBy("mLock")
+    void updateOverlayContainer(@NonNull WindowContainerTransaction wct,
+                                @NonNull TaskFragmentContainer container) {
+        final TaskContainer taskContainer = container.getTaskContainer();
+        // Dismiss the overlay container if it's the only container in the task and there's no
+        // direct activity in the parent task.
+        if (taskContainer.getTaskFragmentContainers().size() == 1
+                && !taskContainer.hasDirectActivity()) {
+            container.finish(false /* shouldFinishDependent */, mPresenter, wct, this);
+        }
+
+        // TODO(b/295805054): Add the logic to update overlay container
+    }
+
     /**
      * Updates {@link SplitContainer} with the given {@link SplitAttributes} if the
      * {@link SplitContainer} is the top most and not finished. If passed {@link SplitAttributes}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index 9e53380..eeb3ccf 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -75,6 +75,8 @@
 
     private boolean mIsVisible;
 
+    private boolean mHasDirectActivity;
+
     /**
      * TaskFragments that the organizer has requested to be closed. They should be removed when
      * the organizer receives
@@ -102,8 +104,9 @@
         mConfiguration = taskProperties.getConfiguration();
         mDisplayId = taskProperties.getDisplayId();
         // Note that it is always called when there's a new Activity is started, which implies
-        // the host task is visible.
+        // the host task is visible and has an activity in the task.
         mIsVisible = true;
+        mHasDirectActivity = true;
     }
 
     int getTaskId() {
@@ -118,6 +121,10 @@
         return mIsVisible;
     }
 
+    boolean hasDirectActivity() {
+        return mHasDirectActivity;
+    }
+
     @NonNull
     TaskProperties getTaskProperties() {
         return new TaskProperties(mDisplayId, mConfiguration);
@@ -127,6 +134,7 @@
         mConfiguration.setTo(info.getConfiguration());
         mDisplayId = info.getDisplayId();
         mIsVisible = info.isVisible();
+        mHasDirectActivity = info.hasDirectActivity();
     }
 
     /**
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
index 405f0b2..e74d5fb 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
@@ -58,6 +58,7 @@
 import android.platform.test.annotations.Presubmit;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.window.TaskFragmentInfo;
+import android.window.TaskFragmentParentInfo;
 import android.window.WindowContainerTransaction;
 
 import androidx.annotation.NonNull;
@@ -413,6 +414,33 @@
                 .isEqualTo(overlayContainer);
     }
 
+    @Test
+    public void testUpdateContainer_dontInvokeUpdateOverlayForNonOverlayContainer() {
+        TaskFragmentContainer taskFragmentContainer = createMockTaskFragmentContainer(mActivity);
+
+        mSplitController.updateContainer(mTransaction, taskFragmentContainer);
+        verify(mSplitController, never()).updateOverlayContainer(any(), any());
+    }
+
+    @Test
+    public void testUpdateOverlayContainer_dismissOverlayIfNeeded() {
+        TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test");
+
+        mSplitController.updateOverlayContainer(mTransaction, overlayContainer);
+
+        final TaskContainer taskContainer = overlayContainer.getTaskContainer();
+        assertThat(taskContainer.getTaskFragmentContainers()).containsExactly(overlayContainer);
+
+        taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(Configuration.EMPTY,
+                DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */));
+
+        mSplitController.updateOverlayContainer(mTransaction, overlayContainer);
+
+        assertWithMessage("The overlay must be dismissed since there's no activity"
+                + " in the task and other taskFragment.")
+                .that(taskContainer.getTaskFragmentContainers()).isEmpty();
+    }
+
     /**
      * A simplified version of {@link SplitController.ActivityStartMonitor
      * #createOrUpdateOverlayTaskFragmentIfNeeded}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index 96839c5..02031a6 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -1139,7 +1139,7 @@
     public void testOnTransactionReady_taskFragmentParentInfoChanged() {
         final TaskFragmentTransaction transaction = new TaskFragmentTransaction();
         final TaskFragmentParentInfo parentInfo = new TaskFragmentParentInfo(Configuration.EMPTY,
-                DEFAULT_DISPLAY, true);
+                DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */);
         transaction.addChange(new TaskFragmentTransaction.Change(
                 TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED)
                 .setTaskId(TASK_ID)
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java
index 2188996..e3f5169 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java
@@ -79,14 +79,14 @@
 
         configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
         taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration,
-                DEFAULT_DISPLAY, true /* visible */));
+                DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */));
 
         assertEquals(WINDOWING_MODE_MULTI_WINDOW,
                 taskContainer.getWindowingModeForSplitTaskFragment(splitBounds));
 
         configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
         taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration,
-                DEFAULT_DISPLAY, true /* visible */));
+                DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */));
 
         assertEquals(WINDOWING_MODE_FREEFORM,
                 taskContainer.getWindowingModeForSplitTaskFragment(splitBounds));
@@ -106,13 +106,13 @@
 
         configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
         taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration,
-                DEFAULT_DISPLAY, true /* visible */));
+                DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */));
 
         assertFalse(taskContainer.isInPictureInPicture());
 
         configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_PINNED);
         taskContainer.updateTaskFragmentParentInfo(new TaskFragmentParentInfo(configuration,
-                DEFAULT_DISPLAY, true /* visible */));
+                DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */));
 
         assertTrue(taskContainer.isInPictureInPicture());
     }
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml
index fa56516..c525a29 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml
@@ -35,8 +35,8 @@
 
         <ImageView
             android:id="@+id/application_icon"
-            android:layout_width="24dp"
-            android:layout_height="24dp"
+            android:layout_width="@dimen/desktop_mode_caption_icon_radius"
+            android:layout_height="@dimen/desktop_mode_caption_icon_radius"
             android:layout_gravity="center_vertical"
             android:contentDescription="@string/app_icon_text" />
 
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
index ee9f070..c6f85a0 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
@@ -22,6 +22,7 @@
     android:orientation="vertical">
 
     <LinearLayout
+        android:id="@+id/app_info_pill"
         android:layout_width="match_parent"
         android:layout_height="@dimen/desktop_mode_handle_menu_app_info_pill_height"
         android:layout_marginTop="@dimen/desktop_mode_handle_menu_margin_top"
@@ -33,10 +34,10 @@
 
         <ImageView
             android:id="@+id/application_icon"
-            android:layout_width="24dp"
-            android:layout_height="24dp"
-            android:layout_marginStart="14dp"
-            android:layout_marginEnd="14dp"
+            android:layout_width="@dimen/desktop_mode_caption_icon_radius"
+            android:layout_height="@dimen/desktop_mode_caption_icon_radius"
+            android:layout_marginStart="12dp"
+            android:layout_marginEnd="12dp"
             android:contentDescription="@string/app_icon_text"/>
 
         <TextView
@@ -66,6 +67,7 @@
     </LinearLayout>
 
     <LinearLayout
+        android:id="@+id/windowing_pill"
         android:layout_width="match_parent"
         android:layout_height="@dimen/desktop_mode_handle_menu_windowing_pill_height"
         android:layout_marginTop="@dimen/desktop_mode_handle_menu_pill_spacing_margin"
@@ -116,6 +118,7 @@
     </LinearLayout>
 
     <LinearLayout
+        android:id="@+id/more_actions_pill"
         android:layout_width="match_parent"
         android:layout_height="@dimen/desktop_mode_handle_menu_more_actions_pill_height"
         android:layout_marginTop="@dimen/desktop_mode_handle_menu_pill_spacing_margin"
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 1f6f7ae..c4be384 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -454,6 +454,9 @@
     <!-- The radius of the caption menu corners. -->
     <dimen name="desktop_mode_handle_menu_corner_radius">26dp</dimen>
 
+    <!-- The radius of the caption menu icon. -->
+    <dimen name="desktop_mode_caption_icon_radius">28dp</dimen>
+
     <!-- The radius of the caption menu shadow. -->
     <dimen name="desktop_mode_handle_menu_shadow_radius">2dp</dimen>
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index ac5ba51e..3660fa2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -57,6 +57,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewOutlineProvider;
+import android.view.ViewPropertyAnimator;
 import android.view.ViewTreeObserver;
 import android.view.WindowManagerPolicyConstants;
 import android.view.accessibility.AccessibilityNodeInfo;
@@ -212,7 +213,8 @@
     private ExpandedViewAnimationController mExpandedViewAnimationController;
 
     private View mScrim;
-    private boolean mScrimAnimating;
+    @Nullable
+    private ViewPropertyAnimator mScrimAnimation;
     private View mManageMenuScrim;
     private FrameLayout mExpandedViewContainer;
 
@@ -748,8 +750,8 @@
             float collapsed = -Math.min(dy, 0);
             mExpandedViewAnimationController.updateDrag((int) collapsed);
 
-            // Update scrim
-            if (!mScrimAnimating) {
+            // Update scrim if it's not animating already
+            if (mScrimAnimation == null) {
                 mScrim.setAlpha(getScrimAlphaForDrag(collapsed));
             }
         }
@@ -768,8 +770,8 @@
             } else {
                 mExpandedViewAnimationController.animateBackToExpanded();
 
-                // Update scrim
-                if (!mScrimAnimating) {
+                // Update scrim if it's not animating already
+                if (mScrimAnimation == null) {
                     showScrim(true, null /* runnable */);
                 }
             }
@@ -2237,26 +2239,27 @@
     private void showScrim(boolean show, Runnable after) {
         AnimatorListenerAdapter listener = new AnimatorListenerAdapter() {
             @Override
-            public void onAnimationStart(Animator animation) {
-                mScrimAnimating = true;
-            }
-
-            @Override
             public void onAnimationEnd(Animator animation) {
-                mScrimAnimating = false;
+                mScrimAnimation = null;
                 if (after != null) {
                     after.run();
                 }
             }
         };
+        if (mScrimAnimation != null) {
+            // Cancel scrim animation if it animates
+            mScrimAnimation.cancel();
+        }
         if (show) {
-            mScrim.animate()
+            mScrimAnimation = mScrim.animate();
+            mScrimAnimation
                     .setInterpolator(ALPHA_IN)
                     .alpha(BUBBLE_EXPANDED_SCRIM_ALPHA)
                     .setListener(listener)
                     .start();
         } else {
-            mScrim.animate()
+            mScrimAnimation = mScrim.animate();
+            mScrimAnimation
                     .alpha(0f)
                     .setInterpolator(ALPHA_OUT)
                     .setListener(listener)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index de03f58..9cd318f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -25,6 +25,7 @@
 import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_DOWN;
 import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_UP;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED;
 import static android.app.admin.DevicePolicyManager.EXTRA_RESOURCE_TYPE;
@@ -515,7 +516,7 @@
         }
 
         if (backgroundColorForTransition != 0) {
-            addBackgroundColorOnTDA(info, backgroundColorForTransition, startTransaction,
+            addBackgroundColor(info, backgroundColorForTransition, startTransaction,
                     finishTransaction);
         }
 
@@ -546,7 +547,7 @@
         return true;
     }
 
-    private void addBackgroundColorOnTDA(@NonNull TransitionInfo info,
+    private void addBackgroundColor(@NonNull TransitionInfo info,
             @ColorInt int color, @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction) {
         final Color bgColor = Color.valueOf(color);
@@ -558,9 +559,19 @@
                     .setName("animation-background")
                     .setCallsite("DefaultTransitionHandler")
                     .setColorLayer();
-
-            mRootTDAOrganizer.attachToDisplayArea(displayId, colorLayerBuilder);
             final SurfaceControl backgroundSurface = colorLayerBuilder.build();
+
+            // Attaching the background surface to the transition root could unexpectedly make it
+            // cover one of the split root tasks. To avoid this, put the background surface just
+            // above the display area when split is on.
+            final boolean isSplitTaskInvolved =
+                    info.getChanges().stream().anyMatch(c-> c.getTaskInfo() != null
+                            && c.getTaskInfo().getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW);
+            if (isSplitTaskInvolved) {
+                mRootTDAOrganizer.attachToDisplayArea(displayId, colorLayerBuilder);
+            } else {
+                startTransaction.reparent(backgroundSurface, info.getRootLeash());
+            }
             startTransaction.setColor(backgroundSurface, colorArray)
                     .setLayer(backgroundSurface, -1)
                     .show(backgroundSurface);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java
index ca2c3b4..293b660 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java
@@ -146,13 +146,13 @@
                 });
             }
         };
+        // If the remote is actually in the same process, then make a copy of parameters since
+        // remote impls assume that they have to clean-up native references.
+        final SurfaceControl.Transaction remoteStartT =
+                copyIfLocal(startTransaction, remote.getRemoteTransition());
+        final TransitionInfo remoteInfo =
+                remoteStartT == startTransaction ? info : info.localRemoteCopy();
         try {
-            // If the remote is actually in the same process, then make a copy of parameters since
-            // remote impls assume that they have to clean-up native references.
-            final SurfaceControl.Transaction remoteStartT =
-                    copyIfLocal(startTransaction, remote.getRemoteTransition());
-            final TransitionInfo remoteInfo =
-                    remoteStartT == startTransaction ? info : info.localRemoteCopy();
             handleDeath(remote.asBinder(), finishCallback);
             remote.getRemoteTransition().startAnimation(transition, remoteInfo, remoteStartT, cb);
             // assume that remote will apply the start transaction.
@@ -160,6 +160,10 @@
             Transitions.setRunningRemoteTransitionDelegate(remote.getAppThread());
         } catch (RemoteException e) {
             Log.e(Transitions.TAG, "Error running remote transition.", e);
+            if (remoteStartT != startTransaction) {
+                remoteStartT.close();
+            }
+            startTransaction.apply();
             unhandleDeath(remote.asBinder(), finishCallback);
             mRequestedRemotes.remove(transition);
             mMainExecutor.execute(() -> finishCallback.onTransitionFinished(null /* wct */));
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index bb262d3..a976584 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -20,6 +20,8 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.windowingModeToString;
 
+import static com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT;
+
 import android.app.ActivityManager;
 import android.app.WindowConfiguration.WindowingMode;
 import android.content.Context;
@@ -27,6 +29,7 @@
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
 import android.content.res.Resources;
+import android.graphics.Bitmap;
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
@@ -44,6 +47,7 @@
 import android.window.WindowContainerTransaction;
 
 import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.launcher3.icons.BaseIconFactory;
 import com.android.launcher3.icons.IconProvider;
 import com.android.wm.shell.R;
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
@@ -93,7 +97,8 @@
 
     private ResizeVeil mResizeVeil;
 
-    private Drawable mAppIcon;
+    private Drawable mAppIconDrawable;
+    private Bitmap mAppIconBitmap;
     private CharSequence mAppName;
 
     private ExclusionRegionListener mExclusionRegionListener;
@@ -255,7 +260,7 @@
                         mOnCaptionButtonClickListener,
                         mOnCaptionLongClickListener,
                         mAppName,
-                        mAppIcon
+                        mAppIconBitmap
                 );
             } else {
                 throw new IllegalArgumentException("Unexpected layout resource id");
@@ -362,10 +367,15 @@
         String packageName = mTaskInfo.realActivity.getPackageName();
         PackageManager pm = mContext.getApplicationContext().getPackageManager();
         try {
-            IconProvider provider = new IconProvider(mContext);
-            mAppIcon = provider.getIcon(pm.getActivityInfo(mTaskInfo.baseActivity,
+            final IconProvider provider = new IconProvider(mContext);
+            mAppIconDrawable = provider.getIcon(pm.getActivityInfo(mTaskInfo.baseActivity,
                     PackageManager.ComponentInfoFlags.of(0)));
-            ApplicationInfo applicationInfo = pm.getApplicationInfo(packageName,
+            final Resources resources = mContext.getResources();
+            final BaseIconFactory factory = new BaseIconFactory(mContext,
+                    resources.getDisplayMetrics().densityDpi,
+                    resources.getDimensionPixelSize(R.dimen.desktop_mode_caption_icon_radius));
+            mAppIconBitmap = factory.createScaledBitmap(mAppIconDrawable, MODE_DEFAULT);
+            final ApplicationInfo applicationInfo = pm.getApplicationInfo(packageName,
                     PackageManager.ApplicationInfoFlags.of(0));
             mAppName = pm.getApplicationLabel(applicationInfo);
         } catch (PackageManager.NameNotFoundException e) {
@@ -386,7 +396,7 @@
      * until a resize event calls showResizeVeil below.
      */
     void createResizeVeil() {
-        mResizeVeil = new ResizeVeil(mContext, mAppIcon, mTaskInfo,
+        mResizeVeil = new ResizeVeil(mContext, mAppIconDrawable, mTaskInfo,
                 mSurfaceControlBuilderSupplier, mDisplay, mSurfaceControlTransactionSupplier);
     }
 
@@ -455,26 +465,29 @@
     }
 
     /**
-     * Create and display handle menu window
+     * Create and display handle menu window.
      */
     void createHandleMenu() {
         mHandleMenu = new HandleMenu.Builder(this)
-                .setAppIcon(mAppIcon)
+                .setAppIcon(mAppIconBitmap)
                 .setAppName(mAppName)
                 .setOnClickListener(mOnCaptionButtonClickListener)
                 .setOnTouchListener(mOnCaptionTouchListener)
                 .setLayoutId(mRelayoutParams.mLayoutResId)
                 .setCaptionPosition(mRelayoutParams.mCaptionX, mRelayoutParams.mCaptionY)
                 .setWindowingButtonsVisible(DesktopModeStatus.isEnabled())
+                .setCaptionHeight(mResult.mCaptionHeight)
                 .build();
+        mWindowDecorViewHolder.onHandleMenuOpened();
         mHandleMenu.show();
     }
 
     /**
-     * Close the handle menu window
+     * Close the handle menu window.
      */
     void closeHandleMenu() {
         if (!isHandleMenuActive()) return;
+        mWindowDecorViewHolder.onHandleMenuClosed();
         mHandleMenu.close();
         mHandleMenu = null;
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java
index 15f8f1c..1941d66 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java
@@ -29,9 +29,9 @@
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
+import android.graphics.Bitmap;
 import android.graphics.Color;
 import android.graphics.PointF;
-import android.graphics.drawable.Drawable;
 import android.view.MotionEvent;
 import android.view.SurfaceControl;
 import android.view.View;
@@ -58,7 +58,7 @@
     private WindowDecoration.AdditionalWindow mHandleMenuWindow;
     private final PointF mHandleMenuPosition = new PointF();
     private final boolean mShouldShowWindowingPill;
-    private final Drawable mAppIcon;
+    private final Bitmap mAppIconBitmap;
     private final CharSequence mAppName;
     private final View.OnClickListener mOnClickListener;
     private final View.OnTouchListener mOnTouchListener;
@@ -71,10 +71,13 @@
     private int mMenuHeight;
     private int mMenuWidth;
 
+    private final int mCaptionHeight;
+
 
     HandleMenu(WindowDecoration parentDecor, int layoutResId, int captionX, int captionY,
             View.OnClickListener onClickListener, View.OnTouchListener onTouchListener,
-            Drawable appIcon, CharSequence appName, boolean shouldShowWindowingPill) {
+            Bitmap appIcon, CharSequence appName, boolean shouldShowWindowingPill,
+            int captionHeight) {
         mParentDecor = parentDecor;
         mContext = mParentDecor.mDecorWindowContext;
         mTaskInfo = mParentDecor.mTaskInfo;
@@ -83,9 +86,10 @@
         mCaptionY = captionY;
         mOnClickListener = onClickListener;
         mOnTouchListener = onTouchListener;
-        mAppIcon = appIcon;
+        mAppIconBitmap = appIcon;
         mAppName = appName;
         mShouldShowWindowingPill = shouldShowWindowingPill;
+        mCaptionHeight = captionHeight;
         loadHandleMenuDimensions();
         updateHandleMenuPillPositions();
     }
@@ -98,6 +102,7 @@
         ssg.addTransaction(t);
         ssg.markSyncReady();
         setupHandleMenu();
+        animateHandleMenu();
     }
 
     private void createHandleMenuWindow(SurfaceControl.Transaction t, SurfaceSyncGroup ssg) {
@@ -109,6 +114,21 @@
     }
 
     /**
+     * Animates the appearance of the handle menu and its three pills.
+     */
+    private void animateHandleMenu() {
+        final View handleMenuView = mHandleMenuWindow.mWindowViewHost.getView();
+        final HandleMenuAnimator handleMenuAnimator = new HandleMenuAnimator(handleMenuView,
+                mMenuWidth, mCaptionHeight);
+        if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN
+                || mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) {
+            handleMenuAnimator.animateCaptionHandleExpandToOpen();
+        } else {
+            handleMenuAnimator.animateOpen();
+        }
+    }
+
+    /**
      * Set up all three pills of the handle menu: app info pill, windowing pill, & more actions
      * pill.
      */
@@ -130,7 +150,7 @@
         final ImageView appIcon = handleMenu.findViewById(R.id.application_icon);
         final TextView appName = handleMenu.findViewById(R.id.application_name);
         collapseBtn.setOnClickListener(mOnClickListener);
-        appIcon.setImageDrawable(mAppIcon);
+        appIcon.setImageBitmap(mAppIconBitmap);
         appName.setText(mAppName);
     }
 
@@ -315,13 +335,14 @@
     static final class Builder {
         private final WindowDecoration mParent;
         private CharSequence mName;
-        private Drawable mAppIcon;
+        private Bitmap mAppIcon;
         private View.OnClickListener mOnClickListener;
         private View.OnTouchListener mOnTouchListener;
         private int mLayoutId;
         private int mCaptionX;
         private int mCaptionY;
         private boolean mShowWindowingPill;
+        private int mCaptionHeight;
 
 
         Builder(@NonNull WindowDecoration parent) {
@@ -333,7 +354,7 @@
             return this;
         }
 
-        Builder setAppIcon(@Nullable Drawable appIcon) {
+        Builder setAppIcon(@Nullable Bitmap appIcon) {
             mAppIcon = appIcon;
             return this;
         }
@@ -364,9 +385,14 @@
             return this;
         }
 
+        Builder setCaptionHeight(int captionHeight) {
+            mCaptionHeight = captionHeight;
+            return this;
+        }
+
         HandleMenu build() {
             return new HandleMenu(mParent, mLayoutId, mCaptionX, mCaptionY, mOnClickListener,
-                    mOnTouchListener, mAppIcon, mName, mShowWindowingPill);
+                    mOnTouchListener, mAppIcon, mName, mShowWindowingPill, mCaptionHeight);
         }
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt
new file mode 100644
index 0000000..531de1f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt
@@ -0,0 +1,245 @@
+/*
+ * 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.windowdecor
+
+import android.animation.Animator
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.view.View
+import android.view.View.ALPHA
+import android.view.View.SCALE_X
+import android.view.View.SCALE_Y
+import android.view.View.TRANSLATION_Y
+import android.view.View.TRANSLATION_Z
+import android.view.ViewGroup
+import androidx.core.view.children
+import com.android.wm.shell.R
+import com.android.wm.shell.animation.Interpolators
+
+/** Animates the Handle Menu opening. */
+class HandleMenuAnimator(
+    private val handleMenu: View,
+    private val menuWidth: Int,
+    private val captionHeight: Float
+) {
+    companion object {
+        private const val MENU_Y_TRANSLATION_DURATION: Long = 150
+        private const val HEADER_NONFREEFORM_SCALE_DURATION: Long = 150
+        private const val HEADER_FREEFORM_SCALE_DURATION: Long = 217
+        private const val HEADER_ELEVATION_DURATION: Long = 83
+        private const val HEADER_CONTENT_ALPHA_DURATION: Long = 100
+        private const val BODY_SCALE_DURATION: Long = 180
+        private const val BODY_ALPHA_DURATION: Long = 150
+        private const val BODY_ELEVATION_DURATION: Long = 83
+        private const val BODY_CONTENT_ALPHA_DURATION: Long = 167
+
+        private const val ELEVATION_DELAY: Long = 33
+        private const val HEADER_CONTENT_ALPHA_DELAY: Long = 67
+        private const val BODY_SCALE_DELAY: Long = 50
+        private const val BODY_ALPHA_DELAY: Long = 133
+
+        private const val HALF_INITIAL_SCALE: Float = 0.5f
+        private const val NONFREEFORM_HEADER_INITIAL_SCALE_X: Float = 0.6f
+        private const val NONFREEFORM_HEADER_INITIAL_SCALE_Y: Float = 0.05f
+    }
+
+    private val animators: MutableList<Animator> = mutableListOf()
+
+    private val appInfoPill: ViewGroup = handleMenu.requireViewById(R.id.app_info_pill)
+    private val windowingPill: ViewGroup = handleMenu.requireViewById(R.id.windowing_pill)
+    private val moreActionsPill: ViewGroup = handleMenu.requireViewById(R.id.more_actions_pill)
+
+    /** Animates the opening of the handle menu. */
+    fun animateOpen() {
+        prepareMenuForAnimation()
+        appInfoPillExpand()
+        animateAppInfoPill()
+        animateWindowingPill()
+        animateMoreActionsPill()
+        runAnimations()
+    }
+
+    /**
+     * Animates the opening of the handle menu. The caption handle in full screen and split screen
+     * will expand until it assumes the shape of the app info pill. Then, the other two pills will
+     * appear.
+     */
+    fun animateCaptionHandleExpandToOpen() {
+        prepareMenuForAnimation()
+        captionHandleExpandIntoAppInfoPill()
+        animateAppInfoPill()
+        animateWindowingPill()
+        animateMoreActionsPill()
+        runAnimations()
+    }
+
+    /**
+     * Prepares the handle menu for animation. Presets the opacity of necessary menu components.
+     * Presets pivots of handle menu and body pills for scaling animation.
+     */
+    private fun prepareMenuForAnimation() {
+        // Preset opacity
+        appInfoPill.children.forEach { it.alpha = 0f }
+        windowingPill.alpha = 0f
+        moreActionsPill.alpha = 0f
+
+        // Setup pivots.
+        handleMenu.pivotX = menuWidth / 2f
+        handleMenu.pivotY = 0f
+
+        windowingPill.pivotX = menuWidth / 2f
+        windowingPill.pivotY = appInfoPill.measuredHeight.toFloat()
+
+        moreActionsPill.pivotX = menuWidth / 2f
+        moreActionsPill.pivotY = appInfoPill.measuredHeight.toFloat()
+    }
+
+    private fun animateAppInfoPill() {
+        // Header Elevation Animation
+        animators +=
+            ObjectAnimator.ofFloat(appInfoPill, TRANSLATION_Z, 1f).apply {
+                startDelay = ELEVATION_DELAY
+                duration = HEADER_ELEVATION_DURATION
+            }
+
+        // Content Opacity Animation
+        appInfoPill.children.forEach {
+            animators +=
+                ObjectAnimator.ofFloat(it, ALPHA, 1f).apply {
+                    startDelay = HEADER_CONTENT_ALPHA_DELAY
+                    duration = HEADER_CONTENT_ALPHA_DURATION
+                }
+        }
+    }
+
+    private fun captionHandleExpandIntoAppInfoPill() {
+        // Header scaling animation
+        animators +=
+            ObjectAnimator.ofFloat(appInfoPill, SCALE_X, NONFREEFORM_HEADER_INITIAL_SCALE_X, 1f)
+                .apply { duration = HEADER_NONFREEFORM_SCALE_DURATION }
+
+        animators +=
+            ObjectAnimator.ofFloat(appInfoPill, SCALE_Y, NONFREEFORM_HEADER_INITIAL_SCALE_Y, 1f)
+                .apply { duration = HEADER_NONFREEFORM_SCALE_DURATION }
+
+        // Downward y-translation animation
+        val yStart: Float = -captionHeight / 2
+        animators +=
+            ObjectAnimator.ofFloat(handleMenu, TRANSLATION_Y, yStart, 0f).apply {
+                duration = MENU_Y_TRANSLATION_DURATION
+            }
+    }
+
+    private fun appInfoPillExpand() {
+        // Header scaling animation
+        animators +=
+            ObjectAnimator.ofFloat(appInfoPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply {
+                duration = HEADER_FREEFORM_SCALE_DURATION
+            }
+
+        animators +=
+            ObjectAnimator.ofFloat(appInfoPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply {
+                duration = HEADER_FREEFORM_SCALE_DURATION
+            }
+    }
+
+    private fun animateWindowingPill() {
+        // Windowing X & Y Scaling Animation
+        animators +=
+            ObjectAnimator.ofFloat(windowingPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply {
+                startDelay = BODY_SCALE_DELAY
+                duration = BODY_SCALE_DURATION
+            }
+
+        animators +=
+            ObjectAnimator.ofFloat(windowingPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply {
+                startDelay = BODY_SCALE_DELAY
+                duration = BODY_SCALE_DURATION
+            }
+
+        // Windowing Opacity Animation
+        animators +=
+            ObjectAnimator.ofFloat(windowingPill, ALPHA, 1f).apply {
+                startDelay = BODY_ALPHA_DELAY
+                duration = BODY_ALPHA_DURATION
+            }
+
+        // Windowing Elevation Animation
+        animators +=
+            ObjectAnimator.ofFloat(windowingPill, TRANSLATION_Z, 1f).apply {
+                startDelay = ELEVATION_DELAY
+                duration = BODY_ELEVATION_DURATION
+            }
+
+        // Windowing Content Opacity Animation
+        windowingPill.children.forEach {
+            animators +=
+                ObjectAnimator.ofFloat(it, ALPHA, 1f).apply {
+                    startDelay = BODY_ALPHA_DELAY
+                    duration = BODY_CONTENT_ALPHA_DURATION
+                    interpolator = Interpolators.FAST_OUT_SLOW_IN
+                }
+        }
+    }
+
+    private fun animateMoreActionsPill() {
+        // More Actions X & Y Scaling Animation
+        animators +=
+            ObjectAnimator.ofFloat(moreActionsPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply {
+                startDelay = BODY_SCALE_DELAY
+                duration = BODY_SCALE_DURATION
+            }
+
+        animators +=
+            ObjectAnimator.ofFloat(moreActionsPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply {
+                startDelay = BODY_SCALE_DELAY
+                duration = BODY_SCALE_DURATION
+            }
+
+        // More Actions Opacity Animation
+        animators +=
+            ObjectAnimator.ofFloat(moreActionsPill, ALPHA, 1f).apply {
+                startDelay = BODY_ALPHA_DELAY
+                duration = BODY_ALPHA_DURATION
+            }
+
+        // More Actions Elevation Animation
+        animators +=
+            ObjectAnimator.ofFloat(moreActionsPill, TRANSLATION_Z, 1f).apply {
+                startDelay = ELEVATION_DELAY
+                duration = BODY_ELEVATION_DURATION
+            }
+
+        // More Actions Content Opacity Animation
+        moreActionsPill.children.forEach {
+            animators +=
+                ObjectAnimator.ofFloat(it, ALPHA, 1f).apply {
+                    startDelay = BODY_ALPHA_DELAY
+                    duration = BODY_CONTENT_ALPHA_DURATION
+                    interpolator = Interpolators.FAST_OUT_SLOW_IN
+                }
+        }
+    }
+
+    /** Runs the list of animators concurrently. */
+    private fun runAnimations() {
+        val animatorSet = AnimatorSet()
+        animatorSet.playTogether(animators)
+        animatorSet.start()
+        animators.clear()
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index 044c033..634b755 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -269,10 +269,10 @@
                     .build();
         }
 
-        final int captionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId);
+        outResult.mCaptionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId);
         final int captionWidth = taskBounds.width();
 
-        startT.setWindowCrop(mCaptionContainerSurface, captionWidth, captionHeight)
+        startT.setWindowCrop(mCaptionContainerSurface, captionWidth, outResult.mCaptionHeight)
                 .setLayer(mCaptionContainerSurface, CAPTION_LAYER_Z_ORDER)
                 .show(mCaptionContainerSurface);
 
@@ -283,7 +283,7 @@
             mCaptionInsetsRect.set(taskBounds);
             if (mIsCaptionVisible) {
                 mCaptionInsetsRect.bottom =
-                        mCaptionInsetsRect.top + captionHeight + params.mCaptionY;
+                        mCaptionInsetsRect.top + outResult.mCaptionHeight + params.mCaptionY;
                 wct.addInsetsSource(mTaskInfo.token,
                         mOwner, 0 /* index */, WindowInsets.Type.captionBar(), mCaptionInsetsRect);
                 wct.addInsetsSource(mTaskInfo.token,
@@ -348,7 +348,7 @@
         // Caption view
         mCaptionWindowManager.setConfiguration(taskConfig);
         final WindowManager.LayoutParams lp =
-                new WindowManager.LayoutParams(captionWidth, captionHeight,
+                new WindowManager.LayoutParams(captionWidth, outResult.mCaptionHeight,
                         WindowManager.LayoutParams.TYPE_APPLICATION,
                         WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT);
         lp.setTitle("Caption of Task=" + mTaskInfo.taskId);
@@ -569,6 +569,7 @@
     }
 
     static class RelayoutResult<T extends View & TaskFocusStateConsumer> {
+        int mCaptionHeight;
         int mWidth;
         int mHeight;
         T mRootView;
@@ -576,6 +577,7 @@
         void reset() {
             mWidth = 0;
             mHeight = 0;
+            mCaptionHeight = 0;
             mRootView = null;
         }
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt
index 6b59cce..d64312a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt
@@ -2,7 +2,7 @@
 
 import android.app.ActivityManager.RunningTaskInfo
 import android.content.res.ColorStateList
-import android.graphics.drawable.Drawable
+import android.graphics.Bitmap
 import android.graphics.drawable.GradientDrawable
 import android.view.View
 import android.view.View.OnLongClickListener
@@ -22,7 +22,7 @@
         onCaptionButtonClickListener: View.OnClickListener,
         onLongClickListener: OnLongClickListener,
         appName: CharSequence,
-        appIcon: Drawable
+        appIconBitmap: Bitmap
 ) : DesktopModeWindowDecorationViewHolder(rootView) {
 
     private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption)
@@ -44,11 +44,10 @@
         maximizeWindowButton.onLongClickListener = onLongClickListener
         closeWindowButton.setOnTouchListener(onCaptionTouchListener)
         appNameTextView.text = appName
-        appIconImageView.setImageDrawable(appIcon)
+        appIconImageView.setImageBitmap(appIconBitmap)
     }
 
     override fun bindData(taskInfo: RunningTaskInfo) {
-
         val captionDrawable = captionView.background as GradientDrawable
         taskInfo.taskDescription?.statusBarColor?.let {
             captionDrawable.setColor(it)
@@ -63,6 +62,10 @@
         appNameTextView.setTextColor(getCaptionAppNameTextColor(taskInfo))
     }
 
+    override fun onHandleMenuOpened() {}
+
+    override fun onHandleMenuClosed() {}
+
     private fun getCaptionAppNameTextColor(taskInfo: RunningTaskInfo): Int {
         return if (shouldUseLightCaptionColors(taskInfo)) {
             context.getColor(R.color.desktop_mode_caption_app_name_light)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt
index 9374ac9..9dc86db 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt
@@ -1,11 +1,13 @@
 package com.android.wm.shell.windowdecor.viewholder
 
+import android.animation.ObjectAnimator
 import android.app.ActivityManager.RunningTaskInfo
 import android.content.res.ColorStateList
 import android.graphics.drawable.GradientDrawable
 import android.view.View
 import android.widget.ImageButton
 import com.android.wm.shell.R
+import com.android.wm.shell.animation.Interpolators
 
 /**
  * A desktop mode window decoration used when the window is in full "focus" (i.e. fullscreen). It
@@ -17,6 +19,10 @@
         onCaptionButtonClickListener: View.OnClickListener
 ) : DesktopModeWindowDecorationViewHolder(rootView) {
 
+    companion object {
+        private const val CAPTION_HANDLE_ANIMATION_DURATION: Long = 100
+    }
+
     private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption)
     private val captionHandle: ImageButton = rootView.requireViewById(R.id.caption_handle)
 
@@ -35,6 +41,14 @@
         captionHandle.imageTintList = ColorStateList.valueOf(getCaptionHandleBarColor(taskInfo))
     }
 
+    override fun onHandleMenuOpened() {
+        animateCaptionHandleAlpha(startValue = 0f, endValue = 1f)
+    }
+
+    override fun onHandleMenuClosed() {
+        animateCaptionHandleAlpha(startValue = 1f, endValue = 0f)
+    }
+
     private fun getCaptionHandleBarColor(taskInfo: RunningTaskInfo): Int {
         return if (shouldUseLightCaptionColors(taskInfo)) {
             context.getColor(R.color.desktop_mode_caption_handle_bar_light)
@@ -42,4 +56,14 @@
             context.getColor(R.color.desktop_mode_caption_handle_bar_dark)
         }
     }
+
+    /** Animate appearance/disappearance of caption handle as the handle menu is animated. */
+    private fun animateCaptionHandleAlpha(startValue: Float, endValue: Float) {
+        val animator =
+            ObjectAnimator.ofFloat(captionHandle, View.ALPHA, startValue, endValue).apply {
+                duration = CAPTION_HANDLE_ANIMATION_DURATION
+                interpolator = Interpolators.FAST_OUT_SLOW_IN
+            }
+        animator.start()
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt
index 49e8d15..8b405f0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt
@@ -35,4 +35,10 @@
           }
         } ?: false
   }
+
+    /** Callback when the handle menu is opened. */
+    abstract fun onHandleMenuOpened()
+
+    /** Callback when the handle menu is closed. */
+    abstract fun onHandleMenuClosed()
 }
diff --git a/media/java/android/media/AudioDeviceAttributes.java b/media/java/android/media/AudioDeviceAttributes.java
index af3c295..5a27435 100644
--- a/media/java/android/media/AudioDeviceAttributes.java
+++ b/media/java/android/media/AudioDeviceAttributes.java
@@ -68,7 +68,7 @@
     /**
      * The unique address of the device. Some devices don't have addresses, only an empty string.
      */
-    private final @NonNull String mAddress;
+    private @NonNull String mAddress;
     /**
      * The non-unique name of the device. Some devices don't have names, only an empty string.
      * Should not be used as a unique identifier for a device.
@@ -188,6 +188,21 @@
 
     /**
      * @hide
+     * Copy Constructor.
+     * @param ada the copied AudioDeviceAttributes
+     */
+    public AudioDeviceAttributes(AudioDeviceAttributes ada) {
+        mRole = ada.getRole();
+        mType = ada.getType();
+        mAddress = ada.getAddress();
+        mName = ada.getName();
+        mNativeType = ada.getInternalType();
+        mAudioProfiles = ada.getAudioProfiles();
+        mAudioDescriptors = ada.getAudioDescriptors();
+    }
+
+    /**
+     * @hide
      * Returns the role of a device
      * @return the role
      */
@@ -218,6 +233,15 @@
 
     /**
      * @hide
+     * Sets the device address. Only used by audio service.
+     */
+    public void setAddress(@NonNull String address) {
+        Objects.requireNonNull(address);
+        mAddress = address;
+    }
+
+    /**
+     * @hide
      * Returns the name of the audio device, or an empty string for devices without one
      * @return the device name
      */
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 5b88079..9ad5c3e 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -20,6 +20,8 @@
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
 import static android.content.Context.DEVICE_ID_DEFAULT;
 
+import static com.android.media.audio.flags.Flags.autoPublicVolumeApiHardening;
+
 import android.Manifest;
 import android.annotation.CallbackExecutor;
 import android.annotation.IntDef;
@@ -1060,8 +1062,17 @@
      * @see #isVolumeFixed()
      */
     public void adjustVolume(int direction, @PublicVolumeFlags int flags) {
-        MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(getContext());
-        helper.sendAdjustVolumeBy(USE_DEFAULT_STREAM_TYPE, direction, flags);
+        if (autoPublicVolumeApiHardening()) {
+            final IAudioService service = getService();
+            try {
+                service.adjustVolume(direction, flags);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        } else {
+            MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(getContext());
+            helper.sendAdjustVolumeBy(USE_DEFAULT_STREAM_TYPE, direction, flags);
+        }
     }
 
     /**
@@ -1090,8 +1101,17 @@
      */
     public void adjustSuggestedStreamVolume(int direction, int suggestedStreamType,
             @PublicVolumeFlags int flags) {
-        MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(getContext());
-        helper.sendAdjustVolumeBy(suggestedStreamType, direction, flags);
+        if (autoPublicVolumeApiHardening()) {
+            final IAudioService service = getService();
+            try {
+                service.adjustSuggestedStreamVolume(direction, suggestedStreamType, flags);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        } else {
+            MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(getContext());
+            helper.sendAdjustVolumeBy(suggestedStreamType, direction, flags);
+        }
     }
 
     /** @hide */
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 0e7718b..8584dbc 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -498,6 +498,10 @@
             in String packageName, int uid, int pid, in UserHandle userHandle,
             int targetSdkVersion);
 
+    oneway void adjustVolume(int direction, int flags);
+
+    oneway void adjustSuggestedStreamVolume(int direction, int suggestedStreamType, int flags);
+
     boolean isMusicActive(in boolean remotely);
 
     int getDeviceMaskForStream(in int streamType);
diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig
index c9cfa67..386534b 100644
--- a/media/java/android/media/flags/media_better_together.aconfig
+++ b/media/java/android/media/flags/media_better_together.aconfig
@@ -34,3 +34,10 @@
     description: "Fallbacks to the default handling for volume adjustment when media session has fixed volume handling and its app is in the foreground and setting a media controller."
     bug: "293743975"
 }
+
+flag {
+    namespace: "media_solutions"
+    name: "enable_waiting_state_for_system_session_creation_request"
+    description: "Introduces a waiting state for the session creation request and prevents it from early failing when the selectedRoute from the bluetooth stack doesn't match the pending request route id."
+    bug: "307723189"
+}
diff --git a/media/java/android/media/projection/IMediaProjectionManager.aidl b/media/java/android/media/projection/IMediaProjectionManager.aidl
index 10c880d..24efbd1 100644
--- a/media/java/android/media/projection/IMediaProjectionManager.aidl
+++ b/media/java/android/media/projection/IMediaProjectionManager.aidl
@@ -158,26 +158,6 @@
             in @nullable IMediaProjection projection);
 
     /**
-     * Notifies system server that we are handling a particular state during the consent flow.
-     *
-     * <p>Only used for emitting atoms.
-     *
-     * @param hostUid               The uid of the process requesting consent to capture, may be an app or
-     *                              SystemUI.
-     * @param state                 The state that SystemUI is handling during the consent flow.
-     *                              Must be a valid
-     *                              state defined in the MediaProjectionState enum.
-     * @param sessionCreationSource Only set if the state is MEDIA_PROJECTION_STATE_INITIATED.
-     *                              Indicates the entry point for requesting the permission. Must be
-     *                              a valid state defined
-     *                              in the SessionCreationSource enum.
-     */
-    @EnforcePermission("android.Manifest.permission.MANAGE_MEDIA_PROJECTION")
-    @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
-            + ".permission.MANAGE_MEDIA_PROJECTION)")
-    oneway void notifyPermissionRequestStateChange(int hostUid, int state, int sessionCreationSource);
-
-    /**
      * Notifies system server that the permission request was initiated.
      *
      * <p>Only used for emitting atoms.
@@ -208,6 +188,19 @@
     oneway void notifyPermissionRequestDisplayed(int hostUid);
 
     /**
+     * Notifies system server that the permission request was cancelled.
+     *
+     * <p>Only used for emitting atoms.
+     *
+     * @param hostUid The uid of the process requesting consent to capture, may be an app or
+     *                SystemUI.
+     */
+    @EnforcePermission("android.Manifest.permission.MANAGE_MEDIA_PROJECTION")
+    @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+            + ".permission.MANAGE_MEDIA_PROJECTION)")
+    oneway void notifyPermissionRequestCancelled(int hostUid);
+
+    /**
      * Notifies system server that the app selector was displayed.
      *
      * <p>Only used for emitting atoms.
diff --git a/packages/CredentialManager/shared/Android.bp b/packages/CredentialManager/shared/Android.bp
index 0d4af2a..47ca944 100644
--- a/packages/CredentialManager/shared/Android.bp
+++ b/packages/CredentialManager/shared/Android.bp
@@ -16,5 +16,6 @@
         "androidx.core_core-ktx",
         "androidx.credentials_credentials",
         "guava",
+        "hilt_android",
     ],
 }
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/repository/PasswordRepository.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/repository/PasswordRepository.kt
index 1cce3ba..5738fee 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/repository/PasswordRepository.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/repository/PasswordRepository.kt
@@ -25,8 +25,11 @@
 import com.android.credentialmanager.TAG
 import com.android.credentialmanager.model.Password
 import com.android.credentialmanager.model.Request
+import javax.inject.Inject
+import javax.inject.Singleton
 
-class PasswordRepository {
+@Singleton
+class PasswordRepository @Inject constructor() {
 
     suspend fun selectPassword(
         password: Password,
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/repository/RequestRepository.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/repository/RequestRepository.kt
index 5ab5ab9..1973fc1 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/repository/RequestRepository.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/repository/RequestRepository.kt
@@ -16,17 +16,20 @@
 
 package com.android.credentialmanager.repository
 
-import android.app.Application
 import android.content.Intent
+import android.content.pm.PackageManager
 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
+import javax.inject.Inject
+import javax.inject.Singleton
 
-class RequestRepository(
-    private val application: Application,
+@Singleton
+class RequestRepository @Inject constructor(
+        private val packageManager: PackageManager,
 ) {
 
     private val _requests = MutableStateFlow<Request?>(null)
@@ -34,7 +37,7 @@
 
     suspend fun processRequest(intent: Intent, previousIntent: Intent? = null) {
         val request = intent.parse(
-            packageManager = application.packageManager,
+            packageManager = packageManager,
             previousIntent = previousIntent
         )
 
diff --git a/packages/CredentialManager/wear/Android.bp b/packages/CredentialManager/wear/Android.bp
index e5f5cc2..c883b1f2 100644
--- a/packages/CredentialManager/wear/Android.bp
+++ b/packages/CredentialManager/wear/Android.bp
@@ -22,6 +22,7 @@
 
     static_libs: [
         "CredentialManagerShared",
+        "hilt_android",
         "Horologist",
         "PlatformComposeCore",
         "androidx.activity_activity-compose",
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt
index 273d0b1..0a63cb7 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt
@@ -29,13 +29,13 @@
 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 dagger.hilt.android.AndroidEntryPoint
 import kotlinx.coroutines.launch
 
-class CredentialSelectorActivity : ComponentActivity() {
+@AndroidEntryPoint(ComponentActivity::class)
+class CredentialSelectorActivity : Hilt_CredentialSelectorActivity() {
 
-    private val viewModel: CredentialSelectorViewModel by viewModels {
-        CredentialSelectorViewModel.Factory
-    }
+    private val viewModel: CredentialSelectorViewModel by viewModels()
 
     @OptIn(ExperimentalHorologistApi::class)
     override fun onCreate(savedInstanceState: Bundle?) {
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorApp.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorApp.kt
index e8e4033..6bd166e 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorApp.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorApp.kt
@@ -17,18 +17,7 @@
 package com.android.credentialmanager
 
 import android.app.Application
-import com.android.credentialmanager.di.inject
-import com.android.credentialmanager.repository.PasswordRepository
-import com.android.credentialmanager.repository.RequestRepository
+import dagger.hilt.android.HiltAndroidApp
 
-class CredentialSelectorApp : Application() {
-
-    lateinit var requestRepository: RequestRepository
-    lateinit var passwordRepository: PasswordRepository
-
-    override fun onCreate() {
-        super.onCreate()
-
-        inject()
-    }
-}
\ No newline at end of file
+@HiltAndroidApp(Application::class)
+class CredentialSelectorApp : Hilt_CredentialSelectorApp()
\ 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
index d557dc0..435cd37 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
@@ -18,20 +18,20 @@
 
 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 dagger.hilt.android.lifecycle.HiltViewModel
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
+import javax.inject.Inject
 
-class CredentialSelectorViewModel(
+@HiltViewModel
+class CredentialSelectorViewModel @Inject constructor(
     private val requestRepository: RequestRepository,
 ) : ViewModel() {
 
@@ -56,22 +56,6 @@
             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 {
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/di/AppModule.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/di/AppModule.kt
new file mode 100644
index 0000000..cb1a4a1
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/di/AppModule.kt
@@ -0,0 +1,18 @@
+package com.android.credentialmanager.di
+
+import android.content.Context
+import android.content.pm.PackageManager
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+@Module
+@InstallIn(SingletonComponent::class)
+internal object AppModule {
+    @Provides
+    @JvmStatic
+    fun providePackageManager(@ApplicationContext context: Context): PackageManager =
+            context.packageManager
+}
+
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/di/DI.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/di/DI.kt
deleted file mode 100644
index 1e8f83d..0000000
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/di/DI.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.android.credentialmanager.di
-
-import android.app.Application
-import com.android.credentialmanager.CredentialSelectorApp
-import com.android.credentialmanager.repository.PasswordRepository
-import com.android.credentialmanager.repository.RequestRepository
-
-// TODO b/301601582 add Hilt for dependency injection
-
-fun CredentialSelectorApp.inject() {
-    requestRepository = requestRepository(application = this)
-    passwordRepository = passwordRepository()
-}
-
-private fun requestRepository(
-    application: Application,
-): RequestRepository = RequestRepository(
-    application = application,
-)
-
-private fun passwordRepository(): PasswordRepository = PasswordRepository()
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
index c87cfd3..81a0672 100644
--- 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
@@ -47,8 +47,7 @@
     columnState: ScalingLazyColumnState,
     onCloseApp: () -> Unit,
     modifier: Modifier = Modifier,
-    viewModel: SinglePasswordScreenViewModel =
-        viewModel(factory = SinglePasswordScreenViewModel.Factory),
+    viewModel: SinglePasswordScreenViewModel = viewModel(),
 ) {
     viewModel.initialize()
 
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
index 3167e67..43514a0 100644
--- 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
@@ -21,11 +21,7 @@
 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.TAG
 import com.android.credentialmanager.ktx.getIntentSenderRequest
 import com.android.credentialmanager.model.Password
@@ -33,12 +29,15 @@
 import com.android.credentialmanager.repository.PasswordRepository
 import com.android.credentialmanager.repository.RequestRepository
 import com.android.credentialmanager.ui.model.PasswordUiModel
+import dagger.hilt.android.lifecycle.HiltViewModel
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.launch
+import javax.inject.Inject
 
-class SinglePasswordScreenViewModel(
+@HiltViewModel
+class SinglePasswordScreenViewModel @Inject constructor(
     private val requestRepository: RequestRepository,
     private val passwordRepository: PasswordRepository,
 ) : ViewModel() {
@@ -105,23 +104,6 @@
             _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,
-                    passwordRepository = application.passwordRepository,
-                ) as T
-            }
-        }
-    }
 }
 
 sealed class SinglePasswordScreenUiState {
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java
index 155cfbb..b633337 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java
@@ -173,13 +173,12 @@
         return mCoordinatorLayout;
     }
 
-    /** Sets the title on the collapsing layout, delegating to host if needed. */
+    /** Sets the title on the collapsing layout and delegates to host. */
     public void setTitle(CharSequence title) {
         if (mCollapsingToolbarLayout != null) {
             mCollapsingToolbarLayout.setTitle(title);
-        } else {
-            mHostCallback.setOuterTitle(title);
         }
+        mHostCallback.setOuterTitle(title);
     }
 
     /** Returns an instance of collapsing toolbar. */
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 9687674..6eaabbb 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1098,19 +1098,6 @@
     <!-- Used to let users know that they have more than some amount of battery life remaining. ex: more than 1 day remaining [CHAR LIMIT = 40] -->
     <string name="power_remaining_only_more_than_subtext">More than <xliff:g id="time_remaining">%1$s</xliff:g> left</string>
 
-    <!-- [CHAR_LIMIT=50] Short label for imminent shutdown warning of device -->
-    <string name="power_remaining_duration_only_shutdown_imminent" product="default">Phone may shut down soon</string>
-    <!-- [CHAR_LIMIT=50] Short label for imminent shutdown warning of device -->
-    <string name="power_remaining_duration_only_shutdown_imminent" product="tablet">Tablet may shut down soon</string>
-    <!-- [CHAR_LIMIT=50] Short label for imminent shutdown warning of device -->
-    <string name="power_remaining_duration_only_shutdown_imminent" product="device">Device may shut down soon</string>
-    <!-- [CHAR_LIMIT=60] Label for battery level chart when shutdown is imminent-->
-    <string name="power_remaining_duration_shutdown_imminent" product="default">Phone may shut down soon (<xliff:g id="level">%1$s</xliff:g>)</string>
-    <!-- [CHAR_LIMIT=60] Label for battery level chart when shutdown is imminent-->
-    <string name="power_remaining_duration_shutdown_imminent" product="tablet">Tablet may shut down soon (<xliff:g id="level">%1$s</xliff:g>)</string>
-    <!-- [CHAR_LIMIT=60] Label for battery level chart when shutdown is imminent-->
-    <string name="power_remaining_duration_shutdown_imminent" product="device">Device may shut down soon (<xliff:g id="level">%1$s</xliff:g>)</string>
-
     <!-- [CHAR_LIMIT=40] Label for battery level chart when charging -->
     <string name="power_charging"><xliff:g id="level">%1$s</xliff:g> - <xliff:g id="state">%2$s</xliff:g></string>
     <!-- [CHAR_LIMIT=40] Label for estimated remaining duration of battery charging -->
diff --git a/packages/SettingsLib/src/com/android/settingslib/PrimarySwitchPreference.java b/packages/SettingsLib/src/com/android/settingslib/PrimarySwitchPreference.java
index 363e20aa..7fbd35b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/PrimarySwitchPreference.java
+++ b/packages/SettingsLib/src/com/android/settingslib/PrimarySwitchPreference.java
@@ -19,7 +19,7 @@
 import android.content.Context;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
-import android.widget.Switch;
+import android.widget.CompoundButton;
 
 import androidx.annotation.Keep;
 import androidx.annotation.Nullable;
@@ -35,7 +35,7 @@
  */
 public class PrimarySwitchPreference extends RestrictedPreference {
 
-    private Switch mSwitch;
+    private CompoundButton mSwitch;
     private boolean mChecked;
     private boolean mCheckedSet;
     private boolean mEnableSwitch = true;
@@ -65,7 +65,7 @@
     @Override
     public void onBindViewHolder(PreferenceViewHolder holder) {
         super.onBindViewHolder(holder);
-        mSwitch = (Switch) holder.findViewById(R.id.switchWidget);
+        mSwitch = (CompoundButton) holder.findViewById(R.id.switchWidget);
         if (mSwitch != null) {
             mSwitch.setOnClickListener(v -> {
                 if (mSwitch != null && !mSwitch.isEnabled()) {
@@ -153,7 +153,7 @@
         setSwitchEnabled(admin == null);
     }
 
-    public Switch getSwitch() {
+    public CompoundButton getSwitch() {
         return mSwitch;
     }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java
index 758f090..60321eb 100644
--- a/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java
+++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java
@@ -36,18 +36,17 @@
 import android.widget.TextView;
 
 import androidx.annotation.VisibleForTesting;
-import androidx.core.content.res.TypedArrayUtils;
 import androidx.preference.PreferenceManager;
 import androidx.preference.PreferenceViewHolder;
-import androidx.preference.SwitchPreference;
+import androidx.preference.SwitchPreferenceCompat;
 
 import com.android.settingslib.utils.BuildCompatUtils;
 
 /**
- * Version of SwitchPreference that can be disabled by a device admin
+ * Version of SwitchPreferenceCompat that can be disabled by a device admin
  * using a user restriction.
  */
-public class RestrictedSwitchPreference extends SwitchPreference {
+public class RestrictedSwitchPreference extends SwitchPreferenceCompat {
     RestrictedPreferenceHelper mHelper;
     AppOpsManager mAppOpsManager;
     boolean mUseAdditionalSummary = false;
@@ -93,8 +92,7 @@
     }
 
     public RestrictedSwitchPreference(Context context, AttributeSet attrs) {
-        this(context, attrs, TypedArrayUtils.getAttr(context, R.attr.switchPreferenceStyle,
-                android.R.attr.switchPreferenceStyle));
+        this(context, attrs, androidx.preference.R.attr.switchPreferenceCompatStyle);
     }
 
     public RestrictedSwitchPreference(Context context) {
@@ -113,7 +111,7 @@
     @Override
     public void onBindViewHolder(PreferenceViewHolder holder) {
         super.onBindViewHolder(holder);
-        final View switchView = holder.findViewById(android.R.id.switch_widget);
+        final View switchView = holder.findViewById(androidx.preference.R.id.switchWidget);
         if (switchView != null) {
             final View rootView = switchView.getRootView();
             rootView.setFilterTouchesWhenObscured(true);
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java
index 660090d..1900575 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java
@@ -19,14 +19,19 @@
 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
 
+import android.annotation.CallbackExecutor;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothClass;
+import android.bluetooth.BluetoothCsipSetCoordinator;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHapClient;
+import android.bluetooth.BluetoothHapPresetInfo;
 import android.bluetooth.BluetoothManager;
 import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothStatusCodes;
 import android.content.Context;
 import android.util.Log;
 
@@ -36,6 +41,7 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.Executor;
 
 /**
  * HapClientProfile handles the Bluetooth HAP service client role.
@@ -118,7 +124,52 @@
     }
 
     /**
-     * Get hearing aid devices matching connection states{
+     * Registers a {@link BluetoothHapClient.Callback} that will be invoked during the
+     * operation of this profile.
+     *
+     * Repeated registration of the same <var>callback</var> object after the first call to this
+     * method will result with IllegalArgumentException being thrown, even when the
+     * <var>executor</var> is different. API caller would have to call
+     * {@link #unregisterCallback(BluetoothHapClient.Callback)} with the same callback object
+     * before registering it again.
+     *
+     * @param executor an {@link Executor} to execute given callback
+     * @param callback user implementation of the {@link BluetoothHapClient.Callback}
+     * @throws NullPointerException if a null executor, or callback is given, or
+     *  IllegalArgumentException if the same <var>callback</var> is already registered.
+     * @hide
+     */
+    public void registerCallback(@NonNull @CallbackExecutor Executor executor,
+            @NonNull BluetoothHapClient.Callback callback) {
+        if (mService == null) {
+            Log.w(TAG, "Proxy not attached to service. Cannot register callback.");
+            return;
+        }
+        mService.registerCallback(executor, callback);
+    }
+
+    /**
+     * Unregisters the specified {@link BluetoothHapClient.Callback}.
+     * <p>The same {@link BluetoothHapClient.Callback} object used when calling
+     * {@link #registerCallback(Executor, BluetoothHapClient.Callback)} must be used.
+     *
+     * <p>Callbacks are automatically unregistered when application process goes away
+     *
+     * @param callback user implementation of the {@link BluetoothHapClient.Callback}
+     * @throws NullPointerException when callback is null or IllegalArgumentException when no
+     *  callback is registered
+     * @hide
+     */
+    public void unregisterCallback(@NonNull BluetoothHapClient.Callback callback) {
+        if (mService == null) {
+            Log.w(TAG, "Proxy not attached to service. Cannot unregister callback.");
+            return;
+        }
+        mService.unregisterCallback(callback);
+    }
+
+    /**
+     * Gets hearing aid devices matching connection states{
      * {@code BluetoothProfile.STATE_CONNECTED},
      * {@code BluetoothProfile.STATE_CONNECTING},
      * {@code BluetoothProfile.STATE_DISCONNECTING}}
@@ -133,7 +184,7 @@
     }
 
     /**
-     * Get hearing aid devices matching connection states{
+     * Gets hearing aid devices matching connection states{
      * {@code BluetoothProfile.STATE_DISCONNECTED},
      * {@code BluetoothProfile.STATE_CONNECTED},
      * {@code BluetoothProfile.STATE_CONNECTING},
@@ -222,6 +273,270 @@
         return mService.supportsWritablePresets(device);
     }
 
+
+    /**
+     * Gets the group identifier, which can be used in the group related part of the API.
+     *
+     * <p>Users are expected to get group identifier for each of the connected device to discover
+     * the device grouping. This allows them to make an informed decision which devices can be
+     * controlled by single group API call and which require individual device calls.
+     *
+     * <p>Note that some binaural HA devices may not support group operations, therefore are not
+     * considered a valid HAP group. In such case -1 is returned even if such device is a valid Le
+     * Audio Coordinated Set member.
+     *
+     * @param device is the device for which we want to get the hap group identifier
+     * @return valid group identifier or -1
+     * @hide
+     */
+    public int getHapGroup(@NonNull BluetoothDevice device) {
+        if (mService == null) {
+            Log.w(TAG, "Proxy not attached to service. Cannot get hap group.");
+            return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
+        }
+        return mService.getHapGroup(device);
+    }
+
+    /**
+     * Gets the currently active preset for a HA device.
+     *
+     * @param device is the device for which we want to set the active preset
+     * @return active preset index or {@link BluetoothHapClient#PRESET_INDEX_UNAVAILABLE} if the
+     *         device is not connected.
+     * @hide
+     */
+    public int getActivePresetIndex(@NonNull BluetoothDevice device) {
+        if (mService == null) {
+            Log.w(TAG, "Proxy not attached to service. Cannot get active preset index.");
+            return BluetoothHapClient.PRESET_INDEX_UNAVAILABLE;
+        }
+        return mService.getActivePresetIndex(device);
+    }
+
+    /**
+     * Gets the currently active preset info for a remote device.
+     *
+     * @param device is the device for which we want to get the preset name
+     * @return currently active preset info if selected, null if preset info is not available for
+     *     the remote device
+     * @hide
+     */
+    @Nullable
+    public BluetoothHapPresetInfo getActivePresetInfo(@NonNull BluetoothDevice device) {
+        if (mService == null) {
+            Log.w(TAG, "Proxy not attached to service. Cannot get active preset info.");
+            return null;
+        }
+        return mService.getActivePresetInfo(device);
+    }
+
+    /**
+     * Selects the currently active preset for a HA device
+     *
+     * <p>On success,
+     * {@link BluetoothHapClient.Callback#onPresetSelected(BluetoothDevice, int, int)} will be
+     * called with reason code {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST} On failure,
+     * {@link BluetoothHapClient.Callback#onPresetSelectionFailed(BluetoothDevice, int)} will be
+     * called.
+     *
+     * @param device is the device for which we want to set the active preset
+     * @param presetIndex is an index of one of the available presets
+     * @hide
+     */
+    public void selectPreset(@NonNull BluetoothDevice device, int presetIndex) {
+        if (mService == null) {
+            Log.w(TAG, "Proxy not attached to service. Cannot select preset.");
+            return;
+        }
+        mService.selectPreset(device, presetIndex);
+    }
+
+
+    /**
+     * Selects the currently active preset for a Hearing Aid device group.
+     *
+     * <p>This group call may replace multiple device calls if those are part of the valid HAS
+     * group. Note that binaural HA devices may or may not support group.
+     *
+     * <p>On success,
+     * {@link BluetoothHapClient.Callback#onPresetSelected(BluetoothDevice, int, int)} will be
+     * called for each device within the group with reason code
+     * {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST} On failure,
+     * {@link BluetoothHapClient.Callback#onPresetSelectionForGroupFailed(int, int)} will be
+     * called for the group.
+     *
+     * @param groupId is the device group identifier for which want to set the active preset
+     * @param presetIndex is an index of one of the available presets
+     * @hide
+     */
+    public void selectPresetForGroup(int groupId, int presetIndex) {
+        if (mService == null) {
+            Log.w(TAG, "Proxy not attached to service. Cannot select preset for group.");
+            return;
+        }
+        mService.selectPresetForGroup(groupId, presetIndex);
+    }
+
+    /**
+     * Sets the next preset as a currently active preset for a HA device
+     *
+     * <p>Note that the meaning of 'next' is HA device implementation specific and does not
+     * necessarily mean a higher preset index.
+     *
+     * @param device is the device for which we want to set the active preset
+     * @hide
+     */
+    public void switchToNextPreset(@NonNull BluetoothDevice device) {
+        if (mService == null) {
+            Log.w(TAG, "Proxy not attached to service. Cannot switch to next preset.");
+            return;
+        }
+        mService.switchToNextPreset(device);
+    }
+
+
+    /**
+     * Sets the next preset as a currently active preset for a HA device group
+     *
+     * <p>Note that the meaning of 'next' is HA device implementation specific and does not
+     * necessarily mean a higher preset index.
+     *
+     * <p>This group call may replace multiple device calls if those are part of the valid HAS
+     * group. Note that binaural HA devices may or may not support group.
+     *
+     * @param groupId is the device group identifier for which want to set the active preset
+     * @hide
+     */
+    public void switchToNextPresetForGroup(int groupId) {
+        if (mService == null) {
+            Log.w(TAG, "Proxy not attached to service. Cannot switch to next preset for group.");
+            return;
+        }
+        mService.switchToNextPresetForGroup(groupId);
+    }
+
+    /**
+     * Sets the previous preset as a currently active preset for a HA device.
+     *
+     * <p>Note that the meaning of 'previous' is HA device implementation specific and does not
+     * necessarily mean a lower preset index.
+     *
+     * @param device is the device for which we want to set the active preset
+     * @hide
+     */
+    public void switchToPreviousPreset(@NonNull BluetoothDevice device) {
+        if (mService == null) {
+            Log.w(TAG, "Proxy not attached to service. Cannot switch to previous preset.");
+            return;
+        }
+        mService.switchToPreviousPreset(device);
+    }
+
+
+    /**
+     * Sets the next preset as a currently active preset for a HA device group
+     *
+     * <p>Note that the meaning of 'next' is HA device implementation specific and does not
+     * necessarily mean a higher preset index.
+     *
+     * <p>This group call may replace multiple device calls if those are part of the valid HAS
+     * group. Note that binaural HA devices may or may not support group.
+     *
+     * @param groupId is the device group identifier for which want to set the active preset
+     * @hide
+     */
+    public void switchToPreviousPresetForGroup(int groupId) {
+        if (mService == null) {
+            Log.w(TAG, "Proxy not attached to service. Cannot switch to previous preset for "
+                    + "group.");
+            return;
+        }
+        mService.switchToPreviousPresetForGroup(groupId);
+    }
+
+    /**
+     * Requests the preset info
+     *
+     * @param device is the device for which we want to get the preset name
+     * @param presetIndex is an index of one of the available presets
+     * @return preset info
+     * @hide
+     */
+    public BluetoothHapPresetInfo getPresetInfo(@NonNull BluetoothDevice device, int presetIndex) {
+        if (mService == null) {
+            Log.w(TAG, "Proxy not attached to service. Cannot get preset info.");
+            return null;
+        }
+        return mService.getPresetInfo(device, presetIndex);
+    }
+
+    /**
+     * Gets all preset info for a particular device
+     *
+     * @param device is the device for which we want to get all presets info
+     * @return a list of all known preset info
+     * @hide
+     */
+    @NonNull
+    public List<BluetoothHapPresetInfo> getAllPresetInfo(@NonNull BluetoothDevice device) {
+        if (mService == null) {
+            Log.w(TAG, "Proxy not attached to service. Cannot get all preset info.");
+            return new ArrayList<>();
+        }
+        return mService.getAllPresetInfo(device);
+    }
+
+    /**
+     * Sets the preset name for a particular device
+     *
+     * <p>Note that the name length is restricted to 40 characters.
+     *
+     * <p>On success,
+     * {@link BluetoothHapClient.Callback#onPresetInfoChanged(BluetoothDevice, List, int)} with a
+     * new name will be called and reason code
+     * {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST} On failure,
+     * {@link BluetoothHapClient.Callback#onSetPresetNameFailed(BluetoothDevice, int)} will be
+     * called.
+     *
+     * @param device is the device for which we want to get the preset name
+     * @param presetIndex is an index of one of the available presets
+     * @param name is a new name for a preset, maximum length is 40 characters
+     * @hide
+     */
+    public void setPresetName(@NonNull BluetoothDevice device, int presetIndex,
+            @NonNull String name) {
+        if (mService == null) {
+            Log.w(TAG, "Proxy not attached to service. Cannot set preset name.");
+            return;
+        }
+        mService.setPresetName(device, presetIndex, name);
+    }
+
+    /**
+     * Sets the name for a hearing aid preset.
+     *
+     * <p>Note that the name length is restricted to 40 characters.
+     *
+     * <p>On success,
+     * {@link BluetoothHapClient.Callback#onPresetInfoChanged(BluetoothDevice, List, int)} with a
+     * new name will be called for each device within the group with reason code
+     * {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST} On failure,
+     * {@link BluetoothHapClient.Callback#onSetPresetNameForGroupFailed(int, int)} will be invoked
+     *
+     * @param groupId is the device group identifier
+     * @param presetIndex is an index of one of the available presets
+     * @param name is a new name for a preset, maximum length is 40 characters
+     * @hide
+     */
+    public void setPresetNameForGroup(int groupId, int presetIndex, @NonNull String name) {
+        if (mService == null) {
+            Log.w(TAG, "Proxy not attached to service. Cannot set preset name for group.");
+            return;
+        }
+        mService.setPresetNameForGroup(groupId, presetIndex, name);
+    }
+
+
     @Override
     public boolean accessProfileEnabled() {
         return false;
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/SettingsJankMonitor.kt b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/SettingsJankMonitor.kt
index 02d76304..f988837 100644
--- a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/SettingsJankMonitor.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/SettingsJankMonitor.kt
@@ -37,7 +37,7 @@
     const val MONITORED_ANIMATION_DURATION_MS = 300L
 
     /**
-     * Detects the jank when click on a SwitchPreference.
+     * Detects the jank when click on a TwoStatePreference.
      *
      * @param recyclerView the recyclerView contains the preference
      * @param preference the clicked preference
diff --git a/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodPreference.java b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodPreference.java
index 2999c83..1501e27 100644
--- a/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodPreference.java
+++ b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodPreference.java
@@ -31,8 +31,8 @@
 import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodManager;
 import android.view.inputmethod.InputMethodSubtype;
+import android.widget.CompoundButton;
 import android.widget.ImageView;
-import android.widget.Switch;
 import android.widget.Toast;
 
 import androidx.preference.Preference;
@@ -138,7 +138,7 @@
     @Override
     public void onBindViewHolder(PreferenceViewHolder holder) {
         super.onBindViewHolder(holder);
-        final Switch switchWidget = getSwitch();
+        final CompoundButton switchWidget = getSwitch();
         if (switchWidget != null) {
             // Avoid default behavior in {@link PrimarySwitchPreference#onBindViewHolder}.
             switchWidget.setOnClickListener(v -> {
diff --git a/packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java b/packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java
index 673f243..2272654 100644
--- a/packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java
+++ b/packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java
@@ -44,45 +44,6 @@
     private static final long ONE_MIN_MILLIS = TimeUnit.MINUTES.toMillis(1);
 
     /**
-     * This method produces the text used in various places throughout the system to describe the
-     * remaining battery life of the phone in a consistent manner.
-     *
-     * @param context
-     * @param drainTimeMs The estimated time remaining before the phone dies in milliseconds.
-     * @param percentageString An optional percentage of battery remaining string.
-     * @param basedOnUsage Whether this estimate is based on usage or simple extrapolation.
-     * @return a properly formatted and localized string describing how much time remains
-     * before the battery runs out.
-     */
-    public static String getBatteryRemainingStringFormatted(Context context, long drainTimeMs,
-            @Nullable String percentageString, boolean basedOnUsage) {
-        if (drainTimeMs > 0) {
-            if (drainTimeMs <= SEVEN_MINUTES_MILLIS) {
-                // show a imminent shutdown warning if less than 7 minutes remain
-                return getShutdownImminentString(context, percentageString);
-            } else if (drainTimeMs <= FIFTEEN_MINUTES_MILLIS) {
-                // show a less than 15 min remaining warning if appropriate
-                CharSequence timeString = StringUtil.formatElapsedTime(context,
-                        FIFTEEN_MINUTES_MILLIS,
-                        false /* withSeconds */, false /* collapseTimeUnit */);
-                return getUnderFifteenString(context, timeString, percentageString);
-            } else if (drainTimeMs >= TWO_DAYS_MILLIS) {
-                // just say more than two day if over 48 hours
-                return getMoreThanTwoDaysString(context, percentageString);
-            } else if (drainTimeMs >= ONE_DAY_MILLIS) {
-                // show remaining days & hours if more than a day
-                return getMoreThanOneDayString(context, drainTimeMs,
-                        percentageString, basedOnUsage);
-            } else {
-                // show the time of day we think you'll run out
-                return getRegularTimeRemainingString(context, drainTimeMs,
-                        percentageString, basedOnUsage);
-            }
-        }
-        return null;
-    }
-
-    /**
      * Method to produce a shortened string describing the remaining battery. Suitable for Quick
      * Settings and other areas where space is constrained.
      *
@@ -128,14 +89,6 @@
         }
     }
 
-    private static String getShutdownImminentString(Context context, String percentageString) {
-        return TextUtils.isEmpty(percentageString)
-                ? context.getString(R.string.power_remaining_duration_only_shutdown_imminent)
-                : context.getString(
-                        R.string.power_remaining_duration_shutdown_imminent,
-                        percentageString);
-    }
-
     private static String getUnderFifteenString(Context context, CharSequence timeString,
             String percentageString) {
         return TextUtils.isEmpty(percentageString)
@@ -268,4 +221,4 @@
             return time - remainder + multiple;
         }
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/PrimarySwitchPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/PrimarySwitchPreferenceTest.java
index d9cf9f2..debfa49 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/PrimarySwitchPreferenceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/PrimarySwitchPreferenceTest.java
@@ -23,8 +23,8 @@
 
 import android.content.Context;
 import android.view.LayoutInflater;
+import android.widget.CompoundButton;
 import android.widget.LinearLayout;
-import android.widget.Switch;
 
 import androidx.preference.Preference.OnPreferenceChangeListener;
 import androidx.preference.PreferenceViewHolder;
@@ -62,7 +62,7 @@
 
     @Test
     public void setChecked_shouldUpdateButtonCheckedState() {
-        final Switch toggle = (Switch) mHolder.findViewById(R.id.switchWidget);
+        final CompoundButton toggle = (CompoundButton) mHolder.findViewById(R.id.switchWidget);
         mPreference.onBindViewHolder(mHolder);
 
         mPreference.setChecked(true);
@@ -74,7 +74,7 @@
 
     @Test
     public void setSwitchEnabled_shouldUpdateButtonEnabledState() {
-        final Switch toggle = (Switch) mHolder.findViewById(R.id.switchWidget);
+        final CompoundButton toggle = (CompoundButton) mHolder.findViewById(R.id.switchWidget);
         mPreference.onBindViewHolder(mHolder);
 
         mPreference.setSwitchEnabled(true);
@@ -86,7 +86,7 @@
 
     @Test
     public void setSwitchEnabled_shouldUpdateButtonEnabledState_beforeViewBound() {
-        final Switch toggle = (Switch) mHolder.findViewById(R.id.switchWidget);
+        final CompoundButton toggle = (CompoundButton) mHolder.findViewById(R.id.switchWidget);
 
         mPreference.setSwitchEnabled(false);
         mPreference.onBindViewHolder(mHolder);
@@ -97,7 +97,7 @@
     public void clickWidgetView_shouldToggleButton() {
         assertThat(mWidgetView).isNotNull();
 
-        final Switch toggle = (Switch) mHolder.findViewById(R.id.switchWidget);
+        final CompoundButton toggle = (CompoundButton) mHolder.findViewById(R.id.switchWidget);
         mPreference.onBindViewHolder(mHolder);
 
         toggle.performClick();
@@ -111,7 +111,7 @@
     public void clickWidgetView_shouldNotToggleButtonIfDisabled() {
         assertThat(mWidgetView).isNotNull();
 
-        final Switch toggle = (Switch) mHolder.findViewById(R.id.switchWidget);
+        final CompoundButton toggle = (CompoundButton) mHolder.findViewById(R.id.switchWidget);
         mPreference.onBindViewHolder(mHolder);
         toggle.setEnabled(false);
 
@@ -122,7 +122,7 @@
     @Test
     public void clickWidgetView_shouldNotifyPreferenceChanged() {
 
-        final Switch toggle = (Switch) mHolder.findViewById(R.id.switchWidget);
+        final CompoundButton toggle = (CompoundButton) mHolder.findViewById(R.id.switchWidget);
 
         final OnPreferenceChangeListener listener = mock(OnPreferenceChangeListener.class);
         mPreference.setOnPreferenceChangeListener(listener);
@@ -139,7 +139,7 @@
 
     @Test
     public void setDisabledByAdmin_hasEnforcedAdmin_shouldDisableButton() {
-        final Switch toggle = (Switch) mHolder.findViewById(R.id.switchWidget);
+        final CompoundButton toggle = (CompoundButton) mHolder.findViewById(R.id.switchWidget);
         toggle.setEnabled(true);
         mPreference.onBindViewHolder(mHolder);
 
@@ -149,7 +149,7 @@
 
     @Test
     public void setDisabledByAdmin_noEnforcedAdmin_shouldEnableButton() {
-        final Switch toggle = (Switch) mHolder.findViewById(R.id.switchWidget);
+        final CompoundButton toggle = (CompoundButton) mHolder.findViewById(R.id.switchWidget);
         toggle.setEnabled(false);
         mPreference.onBindViewHolder(mHolder);
 
@@ -159,7 +159,7 @@
 
     @Test
     public void onBindViewHolder_toggleButtonShouldHaveContentDescription() {
-        final Switch toggle = (Switch) mHolder.findViewById(R.id.switchWidget);
+        final CompoundButton toggle = (CompoundButton) mHolder.findViewById(R.id.switchWidget);
         final String label = "TestButton";
         mPreference.setTitle(label);
 
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HapClientProfileTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HapClientProfileTest.java
index 03a792a..7e3263b 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HapClientProfileTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HapClientProfileTest.java
@@ -26,10 +26,12 @@
 
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHapClient;
+import android.bluetooth.BluetoothHapPresetInfo;
 import android.bluetooth.BluetoothManager;
 import android.bluetooth.BluetoothProfile;
 import android.content.Context;
 
+import androidx.annotation.NonNull;
 import androidx.test.core.app.ApplicationProvider;
 
 import com.android.settingslib.testutils.shadow.ShadowBluetoothAdapter;
@@ -47,11 +49,16 @@
 
 import java.util.Arrays;
 import java.util.List;
+import java.util.concurrent.Executor;
 
 @RunWith(RobolectricTestRunner.class)
 @Config(shadows = {ShadowBluetoothAdapter.class})
 public class HapClientProfileTest {
 
+    private static final int TEST_GROUP_ID = 1;
+    private static final int TEST_PRESET_INDEX = 1;
+    private static final String TEST_DEVICE_NAME = "test_device";
+
     @Rule
     public final MockitoRule mockito = MockitoJUnit.rule();
 
@@ -63,6 +70,8 @@
     private BluetoothDevice mBluetoothDevice;
     @Mock
     private BluetoothHapClient mService;
+    @Mock
+    private BluetoothHapPresetInfo mPresetInfo;
 
     private final Context mContext = ApplicationProvider.getApplicationContext();
     private BluetoothProfile.ServiceListener mServiceListener;
@@ -206,4 +215,275 @@
 
         assertThat(mProfile.getConnectableDevices().size()).isEqualTo(connectableList.size());
     }
+
+    /**
+     * Verify registerCallback() call is correctly delegated to {@link BluetoothHapClient} service.
+     */
+    @Test
+    public void registerCallback_verifyIsCalled() {
+        final Executor executor = (command -> new Thread(command).start());
+        final BluetoothHapClient.Callback callback = new BluetoothHapClient.Callback() {
+            @Override
+            public void onPresetSelected(@NonNull BluetoothDevice device, int presetIndex,
+                    int reason) {
+
+            }
+
+            @Override
+            public void onPresetSelectionFailed(@NonNull BluetoothDevice device, int reason) {
+
+            }
+
+            @Override
+            public void onPresetSelectionForGroupFailed(int hapGroupId, int reason) {
+
+            }
+
+            @Override
+            public void onPresetInfoChanged(@NonNull BluetoothDevice device,
+                    @NonNull List<BluetoothHapPresetInfo> presetInfoList, int reason) {
+
+            }
+
+            @Override
+            public void onSetPresetNameFailed(@NonNull BluetoothDevice device, int reason) {
+
+            }
+
+            @Override
+            public void onSetPresetNameForGroupFailed(int hapGroupId, int reason) {
+
+            }
+        };
+        mServiceListener.onServiceConnected(BluetoothProfile.HAP_CLIENT, mService);
+
+        mProfile.registerCallback(executor, callback);
+
+        verify(mService).registerCallback(executor, callback);
+    }
+
+    /**
+     * Verify unregisterCallback() call is correctly delegated to {@link BluetoothHapClient}
+     * service.
+     */
+    @Test
+    public void unregisterCallback_verifyIsCalled() {
+        final BluetoothHapClient.Callback callback = new BluetoothHapClient.Callback() {
+            @Override
+            public void onPresetSelected(@NonNull BluetoothDevice device, int presetIndex,
+                    int reason) {
+
+            }
+
+            @Override
+            public void onPresetSelectionFailed(@NonNull BluetoothDevice device, int reason) {
+
+            }
+
+            @Override
+            public void onPresetSelectionForGroupFailed(int hapGroupId, int reason) {
+
+            }
+
+            @Override
+            public void onPresetInfoChanged(@NonNull BluetoothDevice device,
+                    @NonNull List<BluetoothHapPresetInfo> presetInfoList, int reason) {
+
+            }
+
+            @Override
+            public void onSetPresetNameFailed(@NonNull BluetoothDevice device, int reason) {
+
+            }
+
+            @Override
+            public void onSetPresetNameForGroupFailed(int hapGroupId, int reason) {
+
+            }
+        };
+        mServiceListener.onServiceConnected(BluetoothProfile.HAP_CLIENT, mService);
+
+        mProfile.unregisterCallback(callback);
+
+        verify(mService).unregisterCallback(callback);
+    }
+
+    /**
+     * Verify getHapGroup() call is correctly delegated to {@link BluetoothHapClient} service
+     * and return correct value.
+     */
+    @Test
+    public void getHapGroup_verifyIsCalledAndReturnCorrectValue() {
+        when(mService.getHapGroup(mBluetoothDevice)).thenReturn(TEST_GROUP_ID);
+        mServiceListener.onServiceConnected(BluetoothProfile.HAP_CLIENT, mService);
+
+        final int groupId = mProfile.getHapGroup(mBluetoothDevice);
+
+        verify(mService).getHapGroup(mBluetoothDevice);
+        assertThat(groupId).isEqualTo(TEST_GROUP_ID);
+    }
+
+    /**
+     * Verify getActivePresetIndex() call is correctly delegated to {@link BluetoothHapClient}
+     * service and return correct index.
+     */
+    @Test
+    public void getActivePresetIndex_verifyIsCalledAndReturnCorrectValue() {
+        when(mService.getActivePresetIndex(mBluetoothDevice)).thenReturn(TEST_PRESET_INDEX);
+        mServiceListener.onServiceConnected(BluetoothProfile.HAP_CLIENT, mService);
+
+        final int activeIndex = mProfile.getActivePresetIndex(mBluetoothDevice);
+
+        verify(mService).getActivePresetIndex(mBluetoothDevice);
+        assertThat(activeIndex).isEqualTo(TEST_PRESET_INDEX);
+    }
+
+    /**
+     * Verify getActivePresetInfo() call is correctly delegated to {@link BluetoothHapClient}
+     * service and return correct object.
+     */
+    @Test
+    public void getActivePresetInfo_verifyIsCalledAndReturnCorrectObject() {
+        when(mService.getActivePresetInfo(mBluetoothDevice)).thenReturn(mPresetInfo);
+        mServiceListener.onServiceConnected(BluetoothProfile.HAP_CLIENT, mService);
+
+        final BluetoothHapPresetInfo activeInfo = mProfile.getActivePresetInfo(mBluetoothDevice);
+
+        verify(mService).getActivePresetInfo(mBluetoothDevice);
+        assertThat(activeInfo).isEqualTo(mPresetInfo);
+    }
+
+    /**
+     * Verify selectPreset() call is correctly delegated to {@link BluetoothHapClient} service.
+     */
+    @Test
+    public void selectPreset_verifyIsCalled() {
+        mServiceListener.onServiceConnected(BluetoothProfile.HAP_CLIENT, mService);
+
+        mProfile.selectPreset(mBluetoothDevice, TEST_PRESET_INDEX);
+
+        verify(mService).selectPreset(mBluetoothDevice, TEST_PRESET_INDEX);
+    }
+
+    /**
+     * Verify selectPresetForGroup() call is correctly delegated to {@link BluetoothHapClient}
+     * service.
+     */
+    @Test
+    public void selectPresetForGroup_verifyIsCalled() {
+        mServiceListener.onServiceConnected(BluetoothProfile.HAP_CLIENT, mService);
+
+        mProfile.selectPresetForGroup(TEST_GROUP_ID, TEST_PRESET_INDEX);
+
+        verify(mService).selectPresetForGroup(TEST_GROUP_ID, TEST_PRESET_INDEX);
+    }
+
+    /**
+     * Verify switchToNextPreset() call is correctly delegated to {@link BluetoothHapClient}
+     * service.
+     */
+    @Test
+    public void switchToNextPreset_verifyIsCalled() {
+        mServiceListener.onServiceConnected(BluetoothProfile.HAP_CLIENT, mService);
+
+        mProfile.switchToNextPreset(mBluetoothDevice);
+
+        verify(mService).switchToNextPreset(mBluetoothDevice);
+    }
+
+    /**
+     * Verify switchToNextPresetForGroup() call is correctly delegated to {@link BluetoothHapClient}
+     * service.
+     */
+    @Test
+    public void switchToNextPresetForGroup_verifyIsCalled() {
+        mServiceListener.onServiceConnected(BluetoothProfile.HAP_CLIENT, mService);
+
+        mProfile.switchToNextPresetForGroup(TEST_GROUP_ID);
+
+        verify(mService).switchToNextPresetForGroup(TEST_GROUP_ID);
+    }
+
+    /**
+     * Verify switchToPreviousPreset() call is correctly delegated to {@link BluetoothHapClient}
+     * service.
+     */
+    @Test
+    public void switchToPreviousPreset_verifyIsCalled() {
+        mServiceListener.onServiceConnected(BluetoothProfile.HAP_CLIENT, mService);
+
+        mProfile.switchToPreviousPreset(mBluetoothDevice);
+
+        verify(mService).switchToPreviousPreset(mBluetoothDevice);
+    }
+
+    /**
+     * Verify switchToPreviousPresetForGroup() call is correctly delegated to
+     * {@link BluetoothHapClient} service.
+     */
+    @Test
+    public void switchToPreviousPresetForGroup_verifyIsCalled() {
+        mServiceListener.onServiceConnected(BluetoothProfile.HAP_CLIENT, mService);
+
+        mProfile.switchToPreviousPresetForGroup(TEST_GROUP_ID);
+
+        verify(mService).switchToPreviousPresetForGroup(TEST_GROUP_ID);
+    }
+
+    /**
+     * Verify getPresetInfo() call is correctly delegated to {@link BluetoothHapClient} service and
+     * return correct object.
+     */
+    @Test
+    public void getPresetInfo_verifyIsCalledAndReturnCorrectObject() {
+        when(mService.getPresetInfo(mBluetoothDevice, TEST_PRESET_INDEX)).thenReturn(mPresetInfo);
+        mServiceListener.onServiceConnected(BluetoothProfile.HAP_CLIENT, mService);
+
+        final BluetoothHapPresetInfo info = mProfile.getPresetInfo(mBluetoothDevice,
+                TEST_PRESET_INDEX);
+
+        verify(mService).getPresetInfo(mBluetoothDevice, TEST_PRESET_INDEX);
+        assertThat(info).isEqualTo(mPresetInfo);
+    }
+
+    /**
+     * Verify getAllPresetInfo() call is correctly delegated to {@link BluetoothHapClient} service
+     * and return correct list.
+     */
+    @Test
+    public void getAllPresetInfo_verifyIsCalledAndReturnCorrectList() {
+        final List<BluetoothHapPresetInfo> testList = Arrays.asList(mPresetInfo, mPresetInfo);
+        when(mService.getAllPresetInfo(mBluetoothDevice)).thenReturn(testList);
+        mServiceListener.onServiceConnected(BluetoothProfile.HAP_CLIENT, mService);
+
+        final List<BluetoothHapPresetInfo> infoList = mProfile.getAllPresetInfo(mBluetoothDevice);
+
+        verify(mService).getAllPresetInfo(mBluetoothDevice);
+        assertThat(infoList.size()).isEqualTo(testList.size());
+    }
+
+    /**
+     * Verify setPresetName() call is correctly delegated to {@link BluetoothHapClient} service.
+     */
+    @Test
+    public void setPresetName_verifyIsCalled() {
+        mServiceListener.onServiceConnected(BluetoothProfile.HAP_CLIENT, mService);
+
+        mProfile.setPresetName(mBluetoothDevice, TEST_PRESET_INDEX, TEST_DEVICE_NAME);
+
+        verify(mService).setPresetName(mBluetoothDevice, TEST_PRESET_INDEX, TEST_DEVICE_NAME);
+    }
+
+    /**
+     * Verify setPresetNameForGroup() call is correctly delegated to {@link BluetoothHapClient}
+     * service.
+     */
+    @Test
+    public void setPresetNameForGroup_verifyIsCalled() {
+        mServiceListener.onServiceConnected(BluetoothProfile.HAP_CLIENT, mService);
+
+        mProfile.setPresetNameForGroup(TEST_GROUP_ID, TEST_PRESET_INDEX, TEST_DEVICE_NAME);
+
+        verify(mService).setPresetNameForGroup(TEST_GROUP_ID, TEST_PRESET_INDEX, TEST_DEVICE_NAME);
+    }
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/utils/PowerUtilTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/utils/PowerUtilTest.java
index ae54206..2e7905f 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/utils/PowerUtilTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/utils/PowerUtilTest.java
@@ -59,155 +59,6 @@
     }
 
     @Test
-    public void testGetBatteryRemainingStringFormatted_moreThanFifteenMinutes_withPercentage() {
-        String info = PowerUtil.getBatteryRemainingStringFormatted(mContext,
-                SEVENTEEN_MIN_MILLIS,
-                TEST_BATTERY_LEVEL_10,
-                true /* basedOnUsage */);
-        String info2 = PowerUtil.getBatteryRemainingStringFormatted(mContext,
-                SEVENTEEN_MIN_MILLIS,
-                TEST_BATTERY_LEVEL_10,
-                false /* basedOnUsage */);
-
-        // We only add special mention for the long string
-        // ex: Will last about 1:15 PM based on your usage (10%)
-        assertThat(info).containsMatch(Pattern.compile(
-                NORMAL_CASE_EXPECTED_PREFIX
-                        + TIME_OF_DAY_REGEX
-                        + ENHANCED_SUFFIX
-                        + PERCENTAGE_REGEX));
-        // shortened string should not have extra text
-        // ex: Will last about 1:15 PM (10%)
-        assertThat(info2).containsMatch(Pattern.compile(
-                NORMAL_CASE_EXPECTED_PREFIX
-                        + TIME_OF_DAY_REGEX
-                        + PERCENTAGE_REGEX));
-    }
-
-    @Test
-    public void testGetBatteryRemainingStringFormatted_moreThanFifteenMinutes_noPercentage() {
-        String info = PowerUtil.getBatteryRemainingStringFormatted(mContext,
-                SEVENTEEN_MIN_MILLIS,
-                null /* percentageString */,
-                true /* basedOnUsage */);
-        String info2 = PowerUtil.getBatteryRemainingStringFormatted(mContext,
-                SEVENTEEN_MIN_MILLIS,
-                null /* percentageString */,
-                false /* basedOnUsage */);
-
-        // We only have % when it is provided
-        // ex: Will last about 1:15 PM based on your usage
-        assertThat(info).containsMatch(Pattern.compile(
-                NORMAL_CASE_EXPECTED_PREFIX
-                        + TIME_OF_DAY_REGEX
-                        + ENHANCED_SUFFIX
-                        + "(" + PERCENTAGE_REGEX + "){0}")); // no percentage
-        // shortened string should not have extra text
-        // ex: Will last about 1:15 PM
-        assertThat(info2).containsMatch(Pattern.compile(
-                NORMAL_CASE_EXPECTED_PREFIX
-                        + TIME_OF_DAY_REGEX
-                        + "(" + PERCENTAGE_REGEX + "){0}")); // no percentage
-    }
-
-    @Test
-    public void testGetBatteryRemainingStringFormatted_lessThanSevenMinutes_usesCorrectString() {
-        String info = PowerUtil.getBatteryRemainingStringFormatted(mContext,
-                FIVE_MINUTES_MILLIS,
-                TEST_BATTERY_LEVEL_10 /* percentageString */,
-                true /* basedOnUsage */);
-        String info2 = PowerUtil.getBatteryRemainingStringFormatted(mContext,
-                FIVE_MINUTES_MILLIS,
-                null /* percentageString */,
-                true /* basedOnUsage */);
-
-        // additional battery percentage in this string
-        assertThat(info.contains("may shut down soon (10%)")).isTrue();
-        // shortened string should not have percentage
-        assertThat(info2.contains("may shut down soon")).isTrue();
-    }
-
-    @Test
-    public void testGetBatteryRemainingStringFormatted_betweenSevenAndFifteenMinutes_usesCorrectString() {
-        String info = PowerUtil.getBatteryRemainingStringFormatted(mContext,
-                TEN_MINUTES_MILLIS,
-                null /* percentageString */,
-                true /* basedOnUsage */);
-        String info2 = PowerUtil.getBatteryRemainingStringFormatted(mContext,
-                TEN_MINUTES_MILLIS,
-                TEST_BATTERY_LEVEL_10 /* percentageString */,
-                true /* basedOnUsage */);
-
-        // shortened string should not have percentage
-        assertThat(info).isEqualTo("Less than 15 min left");
-        // Add percentage to string when provided
-        assertThat(info2).isEqualTo("Less than 15 min left (10%)");
-    }
-
-    @Test
-    public void testGetBatteryRemainingStringFormatted_betweenOneAndTwoDays_usesCorrectString() {
-        String info = PowerUtil.getBatteryRemainingStringFormatted(mContext,
-                THIRTY_HOURS_MILLIS,
-                null /* percentageString */,
-                true /* basedOnUsage */);
-        String info2 = PowerUtil.getBatteryRemainingStringFormatted(mContext,
-                THIRTY_HOURS_MILLIS,
-                TEST_BATTERY_LEVEL_10 /* percentageString */,
-                false /* basedOnUsage */);
-        String info3 = PowerUtil.getBatteryRemainingStringFormatted(mContext,
-                THIRTY_HOURS_MILLIS + TEN_MINUTES_MILLIS,
-                null /* percentageString */,
-                false /* basedOnUsage */);
-
-        // We only add special mention for the long string
-        assertThat(info).isEqualTo("About 1 day, 6 hr left based on your usage");
-        // shortened string should not have extra text
-        assertThat(info2).isEqualTo("About 1 day, 6 hr left (10%)");
-        // present 2 time unit at most
-        assertThat(info3).isEqualTo("About 1 day, 6 hr left");
-    }
-
-    @Test
-    public void testGetBatteryRemainingStringFormatted_lessThanOneDay_usesCorrectString() {
-        String info = PowerUtil.getBatteryRemainingStringFormatted(mContext,
-                TEN_HOURS_MILLIS,
-                null /* percentageString */,
-                true /* basedOnUsage */);
-        String info2 = PowerUtil.getBatteryRemainingStringFormatted(mContext,
-                TEN_HOURS_MILLIS,
-                TEST_BATTERY_LEVEL_10 /* percentageString */,
-                false /* basedOnUsage */);
-        String info3 = PowerUtil.getBatteryRemainingStringFormatted(mContext,
-                TEN_HOURS_MILLIS + TEN_MINUTES_MILLIS + TEN_SEC_MILLIS,
-                null /* percentageString */,
-                false /* basedOnUsage */);
-
-        // We only add special mention for the long string
-        assertThat(info).isEqualTo("About 10 hr left based on your usage");
-        // shortened string should not have extra text
-        assertThat(info2).isEqualTo("About 10 hr left (10%)");
-        // present 2 time unit at most
-        assertThat(info3).isEqualTo("About 10 hr, 10 min left");
-    }
-
-    @Test
-    public void testGetBatteryRemainingStringFormatted_moreThanTwoDays_usesCorrectString() {
-        String info = PowerUtil.getBatteryRemainingStringFormatted(mContext,
-                THREE_DAYS_MILLIS,
-                null /* percentageString */,
-                true /* basedOnUsage */);
-        String info2 = PowerUtil.getBatteryRemainingStringFormatted(mContext,
-                THREE_DAYS_MILLIS,
-                TEST_BATTERY_LEVEL_10 /* percentageString */,
-                true /* basedOnUsage */);
-
-        // shortened string should not have percentage
-        assertThat(info).isEqualTo("More than 2 days left");
-        // Add percentage to string when provided
-        assertThat(info2).isEqualTo("More than 2 days left (10%)");
-    }
-
-    @Test
     public void getBatteryTipStringFormatted_moreThanOneDay_usesCorrectString() {
         String info = PowerUtil.getBatteryTipStringFormatted(mContext,
                 THREE_DAYS_MILLIS);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/GenerationRegistry.java b/packages/SettingsProvider/src/com/android/providers/settings/GenerationRegistry.java
index 02ec486..cd35f67 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/GenerationRegistry.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/GenerationRegistry.java
@@ -45,7 +45,8 @@
 
     private static final boolean DEBUG = false;
 
-    private final Object mLock;
+    // This lock is not the same lock used in SettingsProvider and SettingsState
+    private final Object mLock = new Object();
 
     // Key -> backingStore mapping
     @GuardedBy("mLock")
@@ -74,8 +75,7 @@
 
     private final int mMaxNumBackingStore;
 
-    GenerationRegistry(Object lock, int maxNumUsers) {
-        mLock = lock;
+    GenerationRegistry(int maxNumUsers) {
         // Add some buffer to maxNumUsers to accommodate corner cases when the actual number of
         // users in the system exceeds the limit
         maxNumUsers = maxNumUsers + 2;
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index 4e2fad0..5acc1ca 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -40,7 +40,6 @@
 import static com.android.providers.settings.SettingsState.isConfigSettingsKey;
 import static com.android.providers.settings.SettingsState.isGlobalSettingsKey;
 import static com.android.providers.settings.SettingsState.isSecureSettingsKey;
-import static com.android.providers.settings.SettingsState.isSsaidSettingsKey;
 import static com.android.providers.settings.SettingsState.isSystemSettingsKey;
 import static com.android.providers.settings.SettingsState.makeKey;
 
@@ -412,6 +411,9 @@
         SettingsState.cacheSystemPackageNamesAndSystemSignature(getContext());
         synchronized (mLock) {
             mSettingsRegistry.migrateAllLegacySettingsIfNeededLocked();
+            for (UserInfo user : mUserManager.getAliveUsers()) {
+                mSettingsRegistry.ensureSettingsForUserLocked(user.id);
+            }
             mSettingsRegistry.syncSsaidTableOnStartLocked();
         }
         mHandler.post(() -> {
@@ -427,65 +429,53 @@
     public Bundle call(String method, String name, Bundle args) {
         final int requestingUserId = getRequestingUserId(args);
         switch (method) {
-            case Settings.CALL_METHOD_GET_CONFIG: {
+            case Settings.CALL_METHOD_GET_CONFIG -> {
                 Setting setting = getConfigSetting(name);
                 return packageValueForCallResult(SETTINGS_TYPE_CONFIG, name, requestingUserId,
                         setting, isTrackingGeneration(args));
             }
-
-            case Settings.CALL_METHOD_GET_GLOBAL: {
+            case Settings.CALL_METHOD_GET_GLOBAL -> {
                 Setting setting = getGlobalSetting(name);
                 return packageValueForCallResult(SETTINGS_TYPE_GLOBAL, name, requestingUserId,
                         setting, isTrackingGeneration(args));
             }
-
-            case Settings.CALL_METHOD_GET_SECURE: {
+            case Settings.CALL_METHOD_GET_SECURE -> {
                 Setting setting = getSecureSetting(name, requestingUserId);
                 return packageValueForCallResult(SETTINGS_TYPE_SECURE, name, requestingUserId,
                         setting, isTrackingGeneration(args));
             }
-
-            case Settings.CALL_METHOD_GET_SYSTEM: {
+            case Settings.CALL_METHOD_GET_SYSTEM -> {
                 Setting setting = getSystemSetting(name, requestingUserId);
                 return packageValueForCallResult(SETTINGS_TYPE_SYSTEM, name, requestingUserId,
                         setting, isTrackingGeneration(args));
             }
-
-            case Settings.CALL_METHOD_PUT_CONFIG: {
+            case Settings.CALL_METHOD_PUT_CONFIG -> {
                 String value = getSettingValue(args);
                 final boolean makeDefault = getSettingMakeDefault(args);
                 insertConfigSetting(name, value, makeDefault);
-                break;
             }
-
-            case Settings.CALL_METHOD_PUT_GLOBAL: {
+            case Settings.CALL_METHOD_PUT_GLOBAL -> {
                 String value = getSettingValue(args);
                 String tag = getSettingTag(args);
                 final boolean makeDefault = getSettingMakeDefault(args);
                 final boolean overrideableByRestore = getSettingOverrideableByRestore(args);
                 insertGlobalSetting(name, value, tag, makeDefault, requestingUserId, false,
                         overrideableByRestore);
-                break;
             }
-
-            case Settings.CALL_METHOD_PUT_SECURE: {
+            case Settings.CALL_METHOD_PUT_SECURE -> {
                 String value = getSettingValue(args);
                 String tag = getSettingTag(args);
                 final boolean makeDefault = getSettingMakeDefault(args);
                 final boolean overrideableByRestore = getSettingOverrideableByRestore(args);
                 insertSecureSetting(name, value, tag, makeDefault, requestingUserId, false,
                         overrideableByRestore);
-                break;
             }
-
-            case Settings.CALL_METHOD_PUT_SYSTEM: {
+            case Settings.CALL_METHOD_PUT_SYSTEM -> {
                 String value = getSettingValue(args);
                 boolean overrideableByRestore = getSettingOverrideableByRestore(args);
                 insertSystemSetting(name, value, requestingUserId, overrideableByRestore);
-                break;
             }
-
-            case Settings.CALL_METHOD_SET_ALL_CONFIG: {
+            case Settings.CALL_METHOD_SET_ALL_CONFIG -> {
                 String prefix = getSettingPrefix(args);
                 Map<String, String> flags = getSettingFlags(args);
                 Bundle result = new Bundle();
@@ -493,120 +483,96 @@
                         setAllConfigSettings(prefix, flags));
                 return result;
             }
-
-            case Settings.CALL_METHOD_SET_SYNC_DISABLED_MODE_CONFIG: {
+            case Settings.CALL_METHOD_SET_SYNC_DISABLED_MODE_CONFIG -> {
                 final int mode = getSyncDisabledMode(args);
                 setSyncDisabledModeConfig(mode);
-                break;
             }
-
-            case Settings.CALL_METHOD_GET_SYNC_DISABLED_MODE_CONFIG: {
+            case Settings.CALL_METHOD_GET_SYNC_DISABLED_MODE_CONFIG -> {
                 Bundle result = new Bundle();
                 result.putInt(Settings.KEY_CONFIG_GET_SYNC_DISABLED_MODE_RETURN,
                         getSyncDisabledModeConfig());
                 return result;
             }
-
-            case Settings.CALL_METHOD_RESET_CONFIG: {
+            case Settings.CALL_METHOD_RESET_CONFIG -> {
                 final int mode = getResetModeEnforcingPermission(args);
                 String prefix = getSettingPrefix(args);
                 resetConfigSetting(mode, prefix);
-                break;
             }
-
-            case Settings.CALL_METHOD_RESET_GLOBAL: {
+            case Settings.CALL_METHOD_RESET_GLOBAL -> {
                 final int mode = getResetModeEnforcingPermission(args);
                 String tag = getSettingTag(args);
                 resetGlobalSetting(requestingUserId, mode, tag);
-                break;
             }
-
-            case Settings.CALL_METHOD_RESET_SECURE: {
+            case Settings.CALL_METHOD_RESET_SECURE -> {
                 final int mode = getResetModeEnforcingPermission(args);
                 String tag = getSettingTag(args);
                 resetSecureSetting(requestingUserId, mode, tag);
-                break;
             }
-
-            case Settings.CALL_METHOD_RESET_SYSTEM: {
+            case Settings.CALL_METHOD_RESET_SYSTEM -> {
                 final int mode = getResetModeEnforcingPermission(args);
                 String tag = getSettingTag(args);
                 resetSystemSetting(requestingUserId, mode, tag);
-                break;
             }
-
-            case Settings.CALL_METHOD_DELETE_CONFIG: {
-                int rows  = deleteConfigSetting(name) ? 1 : 0;
+            case Settings.CALL_METHOD_DELETE_CONFIG -> {
+                int rows = deleteConfigSetting(name) ? 1 : 0;
                 Bundle result = new Bundle();
                 result.putInt(RESULT_ROWS_DELETED, rows);
                 return result;
             }
-
-            case Settings.CALL_METHOD_DELETE_GLOBAL: {
+            case Settings.CALL_METHOD_DELETE_GLOBAL -> {
                 int rows = deleteGlobalSetting(name, requestingUserId, false) ? 1 : 0;
                 Bundle result = new Bundle();
                 result.putInt(RESULT_ROWS_DELETED, rows);
                 return result;
             }
-
-            case Settings.CALL_METHOD_DELETE_SECURE: {
+            case Settings.CALL_METHOD_DELETE_SECURE -> {
                 int rows = deleteSecureSetting(name, requestingUserId, false) ? 1 : 0;
                 Bundle result = new Bundle();
                 result.putInt(RESULT_ROWS_DELETED, rows);
                 return result;
             }
-
-            case Settings.CALL_METHOD_DELETE_SYSTEM: {
+            case Settings.CALL_METHOD_DELETE_SYSTEM -> {
                 int rows = deleteSystemSetting(name, requestingUserId) ? 1 : 0;
                 Bundle result = new Bundle();
                 result.putInt(RESULT_ROWS_DELETED, rows);
                 return result;
             }
-
-            case Settings.CALL_METHOD_LIST_CONFIG: {
+            case Settings.CALL_METHOD_LIST_CONFIG -> {
                 String prefix = getSettingPrefix(args);
                 Bundle result = packageValuesForCallResult(prefix, getAllConfigFlags(prefix),
                         isTrackingGeneration(args));
                 reportDeviceConfigAccess(prefix);
                 return result;
             }
-
-            case Settings.CALL_METHOD_REGISTER_MONITOR_CALLBACK_CONFIG: {
+            case Settings.CALL_METHOD_REGISTER_MONITOR_CALLBACK_CONFIG -> {
                 RemoteCallback callback = args.getParcelable(
                         Settings.CALL_METHOD_MONITOR_CALLBACK_KEY);
                 setMonitorCallback(callback);
-                break;
             }
-
-            case Settings.CALL_METHOD_UNREGISTER_MONITOR_CALLBACK_CONFIG: {
+            case Settings.CALL_METHOD_UNREGISTER_MONITOR_CALLBACK_CONFIG -> {
                 clearMonitorCallback();
-                break;
             }
-
-            case Settings.CALL_METHOD_LIST_GLOBAL: {
+            case Settings.CALL_METHOD_LIST_GLOBAL -> {
                 Bundle result = new Bundle();
                 result.putStringArrayList(RESULT_SETTINGS_LIST,
                         buildSettingsList(getAllGlobalSettings(null)));
                 return result;
             }
-
-            case Settings.CALL_METHOD_LIST_SECURE: {
+            case Settings.CALL_METHOD_LIST_SECURE -> {
                 Bundle result = new Bundle();
                 result.putStringArrayList(RESULT_SETTINGS_LIST,
                         buildSettingsList(getAllSecureSettings(requestingUserId, null)));
                 return result;
             }
-
-            case Settings.CALL_METHOD_LIST_SYSTEM: {
+            case Settings.CALL_METHOD_LIST_SYSTEM -> {
                 Bundle result = new Bundle();
                 result.putStringArrayList(RESULT_SETTINGS_LIST,
                         buildSettingsList(getAllSystemSettings(requestingUserId, null)));
                 return result;
             }
-
-            default: {
+            default -> {
                 Slog.w(LOG_TAG, "call() with invalid method: " + method);
-            } break;
+            }
         }
 
         return null;
@@ -638,7 +604,7 @@
         }
 
         switch (args.table) {
-            case TABLE_GLOBAL: {
+            case TABLE_GLOBAL -> {
                 if (args.name != null) {
                     Setting setting = getGlobalSetting(args.name);
                     return packageSettingForQuery(setting, normalizedProjection);
@@ -646,8 +612,7 @@
                     return getAllGlobalSettings(projection);
                 }
             }
-
-            case TABLE_SECURE: {
+            case TABLE_SECURE -> {
                 final int userId = UserHandle.getCallingUserId();
                 if (args.name != null) {
                     Setting setting = getSecureSetting(args.name, userId);
@@ -656,8 +621,7 @@
                     return getAllSecureSettings(userId, projection);
                 }
             }
-
-            case TABLE_SYSTEM: {
+            case TABLE_SYSTEM -> {
                 final int userId = UserHandle.getCallingUserId();
                 if (args.name != null) {
                     Setting setting = getSystemSetting(args.name, userId);
@@ -666,8 +630,7 @@
                     return getAllSystemSettings(userId, projection);
                 }
             }
-
-            default: {
+            default -> {
                 throw new IllegalArgumentException("Invalid Uri path:" + uri);
             }
         }
@@ -708,30 +671,27 @@
         String value = values.getAsString(Settings.Secure.VALUE);
 
         switch (table) {
-            case TABLE_GLOBAL: {
+            case TABLE_GLOBAL -> {
                 if (insertGlobalSetting(name, value, null, false,
                         UserHandle.getCallingUserId(), false,
                         /* overrideableByRestore */ false)) {
-                    return Uri.withAppendedPath(Settings.Global.CONTENT_URI, name);
+                    return Uri.withAppendedPath(Global.CONTENT_URI, name);
                 }
-            } break;
-
-            case TABLE_SECURE: {
+            }
+            case TABLE_SECURE -> {
                 if (insertSecureSetting(name, value, null, false,
                         UserHandle.getCallingUserId(), false,
                         /* overrideableByRestore */ false)) {
-                    return Uri.withAppendedPath(Settings.Secure.CONTENT_URI, name);
+                    return Uri.withAppendedPath(Secure.CONTENT_URI, name);
                 }
-            } break;
-
-            case TABLE_SYSTEM: {
+            }
+            case TABLE_SYSTEM -> {
                 if (insertSystemSetting(name, value, UserHandle.getCallingUserId(),
                         /* overridableByRestore */ false)) {
                     return Uri.withAppendedPath(Settings.System.CONTENT_URI, name);
                 }
-            } break;
-
-            default: {
+            }
+            default -> {
                 throw new IllegalArgumentException("Bad Uri path:" + uri);
             }
         }
@@ -775,22 +735,19 @@
         }
 
         switch (args.table) {
-            case TABLE_GLOBAL: {
+            case TABLE_GLOBAL -> {
                 final int userId = UserHandle.getCallingUserId();
                 return deleteGlobalSetting(args.name, userId, false) ? 1 : 0;
             }
-
-            case TABLE_SECURE: {
+            case TABLE_SECURE -> {
                 final int userId = UserHandle.getCallingUserId();
                 return deleteSecureSetting(args.name, userId, false) ? 1 : 0;
             }
-
-            case TABLE_SYSTEM: {
+            case TABLE_SYSTEM -> {
                 final int userId = UserHandle.getCallingUserId();
                 return deleteSystemSetting(args.name, userId) ? 1 : 0;
             }
-
-            default: {
+            default -> {
                 throw new IllegalArgumentException("Bad Uri path:" + uri);
             }
         }
@@ -816,24 +773,21 @@
         String value = values.getAsString(Settings.Secure.VALUE);
 
         switch (args.table) {
-            case TABLE_GLOBAL: {
+            case TABLE_GLOBAL -> {
                 final int userId = UserHandle.getCallingUserId();
                 return updateGlobalSetting(args.name, value, null, false,
                         userId, false) ? 1 : 0;
             }
-
-            case TABLE_SECURE: {
+            case TABLE_SECURE -> {
                 final int userId = UserHandle.getCallingUserId();
                 return updateSecureSetting(args.name, value, null, false,
                         userId, false) ? 1 : 0;
             }
-
-            case TABLE_SYSTEM: {
+            case TABLE_SYSTEM -> {
                 final int userId = UserHandle.getCallingUserId();
                 return updateSystemSetting(args.name, value, userId) ? 1 : 0;
             }
-
-            default: {
+            default -> {
                 throw new IllegalArgumentException("Invalid Uri path:" + uri);
             }
         }
@@ -1034,27 +988,38 @@
 
     private void registerBroadcastReceivers() {
         IntentFilter userFilter = new IntentFilter();
+        userFilter.addAction(Intent.ACTION_USER_ADDED);
         userFilter.addAction(Intent.ACTION_USER_REMOVED);
         userFilter.addAction(Intent.ACTION_USER_STOPPED);
 
         getContext().registerReceiver(new BroadcastReceiver() {
             @Override
             public void onReceive(Context context, Intent intent) {
+                if (intent.getAction() == null) {
+                    return;
+                }
                 final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE,
-                        UserHandle.USER_SYSTEM);
+                        UserHandle.USER_NULL);
+                if (userId == UserHandle.USER_NULL) {
+                    return;
+                }
 
                 switch (intent.getAction()) {
-                    case Intent.ACTION_USER_REMOVED: {
+                    case Intent.ACTION_USER_ADDED -> {
+                        synchronized (mLock) {
+                            mSettingsRegistry.ensureSettingsForUserLocked(userId);
+                        }
+                    }
+                    case Intent.ACTION_USER_REMOVED -> {
                         synchronized (mLock) {
                             mSettingsRegistry.removeUserStateLocked(userId, true);
                         }
-                    } break;
-
-                    case Intent.ACTION_USER_STOPPED: {
+                    }
+                    case Intent.ACTION_USER_STOPPED -> {
                         synchronized (mLock) {
                             mSettingsRegistry.removeUserStateLocked(userId, false);
                         }
-                    } break;
+                    }
                 }
             }
         }, userFilter);
@@ -1350,26 +1315,24 @@
         // Perform the mutation.
         synchronized (mLock) {
             switch (operation) {
-                case MUTATION_OPERATION_INSERT: {
+                case MUTATION_OPERATION_INSERT -> {
                     enforceDeviceConfigWritePermission(getContext(), Collections.singleton(name));
                     return mSettingsRegistry.insertSettingLocked(SETTINGS_TYPE_CONFIG,
                             UserHandle.USER_SYSTEM, name, value, null, makeDefault, true,
                             callingPackage, false, null,
                             /* overrideableByRestore */ false);
                 }
-
-                case MUTATION_OPERATION_DELETE: {
+                case MUTATION_OPERATION_DELETE -> {
                     enforceDeviceConfigWritePermission(getContext(), Collections.singleton(name));
                     return mSettingsRegistry.deleteSettingLocked(SETTINGS_TYPE_CONFIG,
                             UserHandle.USER_SYSTEM, name, false, null);
                 }
-
-                case MUTATION_OPERATION_RESET: {
+                case MUTATION_OPERATION_RESET -> {
                     enforceDeviceConfigWritePermission(getContext(),
                             getAllConfigFlags(prefix).keySet());
-                    mSettingsRegistry.resetSettingsLocked(SETTINGS_TYPE_CONFIG,
+                    return mSettingsRegistry.resetSettingsLocked(SETTINGS_TYPE_CONFIG,
                             UserHandle.USER_SYSTEM, callingPackage, mode, null, prefix);
-                } return true;
+                }
             }
         }
 
@@ -1523,7 +1486,7 @@
         enforceHasAtLeastOnePermission(Manifest.permission.WRITE_SECURE_SETTINGS);
 
         // Resolve the userId on whose behalf the call is made.
-        final int callingUserId = resolveCallingUserIdEnforcingPermissionsLocked(requestingUserId);
+        final int callingUserId = resolveCallingUserIdEnforcingPermissions(requestingUserId);
 
         // If this is a setting that is currently restricted for this user, do not allow
         // unrestricting changes.
@@ -1536,28 +1499,25 @@
         // Perform the mutation.
         synchronized (mLock) {
             switch (operation) {
-                case MUTATION_OPERATION_INSERT: {
+                case MUTATION_OPERATION_INSERT -> {
                     return mSettingsRegistry.insertSettingLocked(SETTINGS_TYPE_GLOBAL,
                             UserHandle.USER_SYSTEM, name, value, tag, makeDefault,
                             callingPackage, forceNotify,
                             CRITICAL_GLOBAL_SETTINGS, overrideableByRestore);
                 }
-
-                case MUTATION_OPERATION_DELETE: {
+                case MUTATION_OPERATION_DELETE -> {
                     return mSettingsRegistry.deleteSettingLocked(SETTINGS_TYPE_GLOBAL,
                             UserHandle.USER_SYSTEM, name, forceNotify, CRITICAL_GLOBAL_SETTINGS);
                 }
-
-                case MUTATION_OPERATION_UPDATE: {
+                case MUTATION_OPERATION_UPDATE -> {
                     return mSettingsRegistry.updateSettingLocked(SETTINGS_TYPE_GLOBAL,
                             UserHandle.USER_SYSTEM, name, value, tag, makeDefault,
                             callingPackage, forceNotify, CRITICAL_GLOBAL_SETTINGS);
                 }
-
-                case MUTATION_OPERATION_RESET: {
-                    mSettingsRegistry.resetSettingsLocked(SETTINGS_TYPE_GLOBAL,
+                case MUTATION_OPERATION_RESET -> {
+                    return mSettingsRegistry.resetSettingsLocked(SETTINGS_TYPE_GLOBAL,
                             UserHandle.USER_SYSTEM, callingPackage, mode, tag);
-                } return true;
+                }
             }
         }
 
@@ -1580,12 +1540,12 @@
         }
 
         // Resolve the userId on whose behalf the call is made.
-        final int callingUserId = resolveCallingUserIdEnforcingPermissionsLocked(userId);
+        final int callingUserId = resolveCallingUserIdEnforcingPermissions(userId);
 
         // The relevant "calling package" userId will be the owning userId for some
         // profiles, and we can't do the lookup inside our [lock held] loop, so work out
         // up front who the effective "new SSAID" user ID for that settings name will be.
-        final int ssaidUserId = resolveOwningUserIdForSecureSettingLocked(callingUserId,
+        final int ssaidUserId = resolveOwningUserIdForSecureSetting(callingUserId,
                 Settings.Secure.ANDROID_ID);
         final PackageInfo ssaidCallingPkg = getCallingPackageInfo(ssaidUserId);
 
@@ -1600,7 +1560,7 @@
             for (int i = 0; i < nameCount; i++) {
                 String name = names.get(i);
                 // Determine the owning user as some profile settings are cloned from the parent.
-                final int owningUserId = resolveOwningUserIdForSecureSettingLocked(callingUserId,
+                final int owningUserId = resolveOwningUserIdForSecureSetting(callingUserId,
                         name);
 
                 if (!isSecureSettingAccessible(name)) {
@@ -1638,13 +1598,13 @@
         }
 
         // Resolve the userId on whose behalf the call is made.
-        final int callingUserId = resolveCallingUserIdEnforcingPermissionsLocked(requestingUserId);
+        final int callingUserId = resolveCallingUserIdEnforcingPermissions(requestingUserId);
 
         // Ensure the caller can access the setting.
         enforceSettingReadable(name, SETTINGS_TYPE_SECURE, UserHandle.getCallingUserId());
 
         // Determine the owning user as some profile settings are cloned from the parent.
-        final int owningUserId = resolveOwningUserIdForSecureSettingLocked(callingUserId, name);
+        final int owningUserId = resolveOwningUserIdForSecureSetting(callingUserId, name);
 
         if (!isSecureSettingAccessible(name)) {
             // This caller is not permitted to access this setting. Pretend the setting doesn't
@@ -1811,7 +1771,7 @@
         enforceHasAtLeastOnePermission(Manifest.permission.WRITE_SECURE_SETTINGS);
 
         // Resolve the userId on whose behalf the call is made.
-        final int callingUserId = resolveCallingUserIdEnforcingPermissionsLocked(requestingUserId);
+        final int callingUserId = resolveCallingUserIdEnforcingPermissions(requestingUserId);
 
         // If this is a setting that is currently restricted for this user, do not allow
         // unrestricting changes.
@@ -1820,7 +1780,7 @@
         }
 
         // Determine the owning user as some profile settings are cloned from the parent.
-        final int owningUserId = resolveOwningUserIdForSecureSettingLocked(callingUserId, name);
+        final int owningUserId = resolveOwningUserIdForSecureSetting(callingUserId, name);
 
         // Only the owning user can change the setting.
         if (owningUserId != callingUserId) {
@@ -1832,28 +1792,25 @@
         // Mutate the value.
         synchronized (mLock) {
             switch (operation) {
-                case MUTATION_OPERATION_INSERT: {
+                case MUTATION_OPERATION_INSERT -> {
                     return mSettingsRegistry.insertSettingLocked(SETTINGS_TYPE_SECURE,
                             owningUserId, name, value, tag, makeDefault,
                             callingPackage, forceNotify, CRITICAL_SECURE_SETTINGS,
                             overrideableByRestore);
                 }
-
-                case MUTATION_OPERATION_DELETE: {
+                case MUTATION_OPERATION_DELETE -> {
                     return mSettingsRegistry.deleteSettingLocked(SETTINGS_TYPE_SECURE,
                             owningUserId, name, forceNotify, CRITICAL_SECURE_SETTINGS);
                 }
-
-                case MUTATION_OPERATION_UPDATE: {
+                case MUTATION_OPERATION_UPDATE -> {
                     return mSettingsRegistry.updateSettingLocked(SETTINGS_TYPE_SECURE,
                             owningUserId, name, value, tag, makeDefault,
                             callingPackage, forceNotify, CRITICAL_SECURE_SETTINGS);
                 }
-
-                case MUTATION_OPERATION_RESET: {
-                    mSettingsRegistry.resetSettingsLocked(SETTINGS_TYPE_SECURE,
+                case MUTATION_OPERATION_RESET -> {
+                    return mSettingsRegistry.resetSettingsLocked(SETTINGS_TYPE_SECURE,
                             UserHandle.USER_SYSTEM, callingPackage, mode, tag);
-                } return true;
+                }
             }
         }
 
@@ -1866,7 +1823,7 @@
         }
 
         // Resolve the userId on whose behalf the call is made.
-        final int callingUserId = resolveCallingUserIdEnforcingPermissionsLocked(userId);
+        final int callingUserId = resolveCallingUserIdEnforcingPermissions(userId);
 
         synchronized (mLock) {
             List<String> names = getSettingsNamesLocked(SETTINGS_TYPE_SYSTEM, callingUserId);
@@ -1903,7 +1860,7 @@
         }
 
         // Resolve the userId on whose behalf the call is made.
-        final int callingUserId = resolveCallingUserIdEnforcingPermissionsLocked(requestingUserId);
+        final int callingUserId = resolveCallingUserIdEnforcingPermissions(requestingUserId);
 
         // Ensure the caller can access the setting.
         enforceSettingReadable(name, SETTINGS_TYPE_SYSTEM, UserHandle.getCallingUserId());
@@ -1978,7 +1935,7 @@
         }
 
         // Resolve the userId on whose behalf the call is made.
-        final int callingUserId = resolveCallingUserIdEnforcingPermissionsLocked(runAsUserId);
+        final int callingUserId = resolveCallingUserIdEnforcingPermissions(runAsUserId);
 
         if (isSettingRestrictedForUser(name, callingUserId, value, Binder.getCallingUid())) {
             Slog.e(LOG_TAG, "UserId: " + callingUserId + " is disallowed to change system "
@@ -2012,37 +1969,30 @@
         // Mutate the value.
         synchronized (mLock) {
             switch (operation) {
-                case MUTATION_OPERATION_INSERT: {
+                case MUTATION_OPERATION_INSERT -> {
                     validateSystemSettingValue(name, value);
                     success = mSettingsRegistry.insertSettingLocked(SETTINGS_TYPE_SYSTEM,
                             owningUserId, name, value, null, false, callingPackage,
                             false, null, overrideableByRestore);
-                    break;
                 }
-
-                case MUTATION_OPERATION_DELETE: {
+                case MUTATION_OPERATION_DELETE -> {
                     success = mSettingsRegistry.deleteSettingLocked(SETTINGS_TYPE_SYSTEM,
                             owningUserId, name, false, null);
-                    break;
                 }
-
-                case MUTATION_OPERATION_UPDATE: {
+                case MUTATION_OPERATION_UPDATE -> {
                     validateSystemSettingValue(name, value);
                     success = mSettingsRegistry.updateSettingLocked(SETTINGS_TYPE_SYSTEM,
                             owningUserId, name, value, null, false, callingPackage,
                             false, null);
-                    break;
                 }
-
-                case MUTATION_OPERATION_RESET: {
+                case MUTATION_OPERATION_RESET -> {
                     success = mSettingsRegistry.resetSettingsLocked(SETTINGS_TYPE_SYSTEM,
                             runAsUserId, callingPackage, mode, tag);
-                    break;
                 }
-
-                default:
+                default -> {
                     success = false;
                     Slog.e(LOG_TAG, "Unknown operation code: " + operation);
+                }
             }
         }
 
@@ -2113,8 +2063,8 @@
      * Returns {@code true} if the specified secure setting should be accessible to the caller.
      */
     private boolean isSecureSettingAccessible(String name) {
-        switch (name) {
-            case "bluetooth_address":
+        return switch (name) {
+            case "bluetooth_address" ->
                 // BluetoothManagerService for some reason stores the Android's Bluetooth MAC
                 // address in this secure setting. Secure settings can normally be read by any app,
                 // which thus enables them to bypass the recently introduced restrictions on access
@@ -2122,22 +2072,23 @@
                 // To mitigate this we make this setting available only to callers privileged to see
                 // this device's MAC addresses, same as through public API
                 // BluetoothAdapter.getAddress() (see BluetoothManagerService for details).
-                return getContext().checkCallingOrSelfPermission(
-                        Manifest.permission.LOCAL_MAC_ADDRESS) == PackageManager.PERMISSION_GRANTED;
-            default:
-                return true;
-        }
+                    getContext().checkCallingOrSelfPermission(Manifest.permission.LOCAL_MAC_ADDRESS)
+                            == PackageManager.PERMISSION_GRANTED;
+            default -> true;
+        };
     }
 
-    private int resolveOwningUserIdForSecureSettingLocked(int userId, String setting) {
-        return resolveOwningUserIdLocked(userId, sSecureCloneToManagedSettings, setting);
+    private int resolveOwningUserIdForSecureSetting(int userId, String setting) {
+        // no need to lock because sSecureCloneToManagedSettings is never modified
+        return resolveOwningUserId(userId, sSecureCloneToManagedSettings, setting);
     }
 
+    @GuardedBy("mLock")
     private int resolveOwningUserIdForSystemSettingLocked(int userId, String setting) {
         final int parentId;
         // Resolves dependency if setting has a dependency and the calling user has a parent
         if (sSystemCloneFromParentOnDependency.containsKey(setting)
-                && (parentId = getGroupParentLocked(userId)) != userId) {
+                && (parentId = getGroupParent(userId)) != userId) {
             // The setting has a dependency and the profile has a parent
             String dependency = sSystemCloneFromParentOnDependency.get(setting);
             // Lookup the dependency setting as ourselves, some callers may not have access to it.
@@ -2151,11 +2102,11 @@
                 Binder.restoreCallingIdentity(token);
             }
         }
-        return resolveOwningUserIdLocked(userId, sSystemCloneToManagedSettings, setting);
+        return resolveOwningUserId(userId, sSystemCloneToManagedSettings, setting);
     }
 
-    private int resolveOwningUserIdLocked(int userId, Set<String> keys, String name) {
-        final int parentId = getGroupParentLocked(userId);
+    private int resolveOwningUserId(int userId, Set<String> keys, String name) {
+        final int parentId = getGroupParent(userId);
         if (parentId != userId && keys.contains(name)) {
             return parentId;
         }
@@ -2174,9 +2125,8 @@
         }
 
         switch (operation) {
-            case MUTATION_OPERATION_INSERT:
-                // Insert updates.
-            case MUTATION_OPERATION_UPDATE: {
+            // Insert updates.
+            case MUTATION_OPERATION_INSERT, MUTATION_OPERATION_UPDATE -> {
                 if (Settings.System.PUBLIC_SETTINGS.contains(name)) {
                     return;
                 }
@@ -2192,9 +2142,8 @@
 
                 warnOrThrowForUndesiredSecureSettingsMutationForTargetSdk(
                         packageInfo.applicationInfo.targetSdkVersion, name);
-            } break;
-
-            case MUTATION_OPERATION_DELETE: {
+            }
+            case MUTATION_OPERATION_DELETE -> {
                 if (Settings.System.PUBLIC_SETTINGS.contains(name)
                         || Settings.System.PRIVATE_SETTINGS.contains(name)) {
                     throw new IllegalArgumentException("You cannot delete system defined"
@@ -2212,34 +2161,26 @@
 
                 warnOrThrowForUndesiredSecureSettingsMutationForTargetSdk(
                         packageInfo.applicationInfo.targetSdkVersion, name);
-            } break;
+            }
         }
     }
 
-    private Set<String> getInstantAppAccessibleSettings(int settingsType) {
-        switch (settingsType) {
-            case SETTINGS_TYPE_GLOBAL:
-                return Settings.Global.INSTANT_APP_SETTINGS;
-            case SETTINGS_TYPE_SECURE:
-                return Settings.Secure.INSTANT_APP_SETTINGS;
-            case SETTINGS_TYPE_SYSTEM:
-                return Settings.System.INSTANT_APP_SETTINGS;
-            default:
-                throw new IllegalArgumentException("Invalid settings type: " + settingsType);
-        }
+    private static Set<String> getInstantAppAccessibleSettings(int settingsType) {
+        return switch (settingsType) {
+            case SETTINGS_TYPE_GLOBAL -> Global.INSTANT_APP_SETTINGS;
+            case SETTINGS_TYPE_SECURE -> Secure.INSTANT_APP_SETTINGS;
+            case SETTINGS_TYPE_SYSTEM -> Settings.System.INSTANT_APP_SETTINGS;
+            default -> throw new IllegalArgumentException("Invalid settings type: " + settingsType);
+        };
     }
 
-    private Set<String> getOverlayInstantAppAccessibleSettings(int settingsType) {
-        switch (settingsType) {
-            case SETTINGS_TYPE_GLOBAL:
-                return OVERLAY_ALLOWED_GLOBAL_INSTANT_APP_SETTINGS;
-            case SETTINGS_TYPE_SYSTEM:
-                return OVERLAY_ALLOWED_SYSTEM_INSTANT_APP_SETTINGS;
-            case SETTINGS_TYPE_SECURE:
-                return OVERLAY_ALLOWED_SECURE_INSTANT_APP_SETTINGS;
-            default:
-                throw new IllegalArgumentException("Invalid settings type: " + settingsType);
-        }
+    private static Set<String> getOverlayInstantAppAccessibleSettings(int settingsType) {
+        return switch (settingsType) {
+            case SETTINGS_TYPE_GLOBAL -> OVERLAY_ALLOWED_GLOBAL_INSTANT_APP_SETTINGS;
+            case SETTINGS_TYPE_SYSTEM -> OVERLAY_ALLOWED_SYSTEM_INSTANT_APP_SETTINGS;
+            case SETTINGS_TYPE_SECURE -> OVERLAY_ALLOWED_SECURE_INSTANT_APP_SETTINGS;
+            default -> throw new IllegalArgumentException("Invalid settings type: " + settingsType);
+        };
     }
 
     @GuardedBy("mLock")
@@ -2270,7 +2211,7 @@
         switch (settingName) {
             // missing READ_PRIVILEGED_PHONE_STATE permission protection
             // see alternative API {@link SubscriptionManager#getPreferredDataSubscriptionId()
-            case Settings.Global.MULTI_SIM_DATA_CALL_SUBSCRIPTION:
+            case Global.MULTI_SIM_DATA_CALL_SUBSCRIPTION -> {
                 // app-compat handling, not break apps targeting on previous SDKs.
                 if (CompatChanges.isChangeEnabled(
                         ENFORCE_READ_PERMISSION_FOR_MULTI_SIM_DATA_CALL)) {
@@ -2278,7 +2219,7 @@
                             Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
                             "access global settings MULTI_SIM_DATA_CALL_SUBSCRIPTION");
                 }
-                break;
+            }
         }
         if (!ai.isInstantApp()) {
             return;
@@ -2306,23 +2247,22 @@
         final Set<String> readableFields;
         final ArrayMap<String, Integer> readableFieldsWithMaxTargetSdk;
         switch (settingsType) {
-            case SETTINGS_TYPE_GLOBAL:
+            case SETTINGS_TYPE_GLOBAL -> {
                 allFields = sAllGlobalSettings;
                 readableFields = sReadableGlobalSettings;
                 readableFieldsWithMaxTargetSdk = sReadableGlobalSettingsWithMaxTargetSdk;
-                break;
-            case SETTINGS_TYPE_SYSTEM:
+            }
+            case SETTINGS_TYPE_SYSTEM -> {
                 allFields = sAllSystemSettings;
                 readableFields = sReadableSystemSettings;
                 readableFieldsWithMaxTargetSdk = sReadableSystemSettingsWithMaxTargetSdk;
-                break;
-            case SETTINGS_TYPE_SECURE:
+            }
+            case SETTINGS_TYPE_SECURE -> {
                 allFields = sAllSecureSettings;
                 readableFields = sReadableSecureSettings;
                 readableFieldsWithMaxTargetSdk = sReadableSecureSettingsWithMaxTargetSdk;
-                break;
-            default:
-                throw new IllegalArgumentException("Invalid settings type: " + settingsType);
+            }
+            default -> throw new IllegalArgumentException("Invalid settings type: " + settingsType);
         }
 
         if (allFields.contains(settingName)) {
@@ -2380,7 +2320,7 @@
         throw new IllegalStateException("Calling package doesn't exist");
     }
 
-    private int getGroupParentLocked(int userId) {
+    private int getGroupParent(int userId) {
         // Most frequent use case.
         if (userId == UserHandle.USER_SYSTEM) {
             return userId;
@@ -2480,7 +2420,7 @@
         }
     }
 
-    private static int resolveCallingUserIdEnforcingPermissionsLocked(int requestingUserId) {
+    private static int resolveCallingUserIdEnforcingPermissions(int requestingUserId) {
         if (requestingUserId == UserHandle.getCallingUserId()) {
             return requestingUserId;
         }
@@ -2654,28 +2594,28 @@
     private static int getResetModeEnforcingPermission(Bundle args) {
         final int mode = (args != null) ? args.getInt(Settings.CALL_METHOD_RESET_MODE_KEY) : 0;
         switch (mode) {
-            case Settings.RESET_MODE_UNTRUSTED_DEFAULTS: {
+            case Settings.RESET_MODE_UNTRUSTED_DEFAULTS -> {
                 if (!isCallerSystemOrShellOrRootOnDebuggableBuild()) {
                     throw new SecurityException("Only system, shell/root on a "
                             + "debuggable build can reset to untrusted defaults");
                 }
                 return mode;
             }
-            case Settings.RESET_MODE_UNTRUSTED_CHANGES: {
+            case Settings.RESET_MODE_UNTRUSTED_CHANGES -> {
                 if (!isCallerSystemOrShellOrRootOnDebuggableBuild()) {
                     throw new SecurityException("Only system, shell/root on a "
                             + "debuggable build can reset untrusted changes");
                 }
                 return mode;
             }
-            case Settings.RESET_MODE_TRUSTED_DEFAULTS: {
+            case Settings.RESET_MODE_TRUSTED_DEFAULTS -> {
                 if (!isCallerSystemOrShellOrRootOnDebuggableBuild()) {
                     throw new SecurityException("Only system, shell/root on a "
                             + "debuggable build can reset to trusted defaults");
                 }
                 return mode;
             }
-            case Settings.RESET_MODE_PACKAGE_DEFAULTS: {
+            case Settings.RESET_MODE_PACKAGE_DEFAULTS -> {
                 return mode;
             }
         }
@@ -2736,21 +2676,18 @@
             String column = cursor.getColumnName(i);
 
             switch (column) {
-                case Settings.NameValueTable._ID: {
+                case Settings.NameValueTable._ID -> {
                     values[i] = setting.getId();
-                } break;
-
-                case Settings.NameValueTable.NAME: {
+                }
+                case Settings.NameValueTable.NAME -> {
                     values[i] = setting.getName();
-                } break;
-
-                case Settings.NameValueTable.VALUE: {
+                }
+                case Settings.NameValueTable.VALUE -> {
                     values[i] = setting.getValue();
-                } break;
-
-                case Settings.NameValueTable.IS_PRESERVED_IN_RESTORE: {
+                }
+                case Settings.NameValueTable.IS_PRESERVED_IN_RESTORE -> {
                     values[i] = String.valueOf(setting.isValuePreservedInRestore());
-                } break;
+                }
             }
         }
 
@@ -2762,19 +2699,11 @@
     }
 
     private String resolveCallingPackage() {
-        switch (Binder.getCallingUid()) {
-            case Process.ROOT_UID: {
-                return "root";
-            }
-
-            case Process.SHELL_UID: {
-                return "com.android.shell";
-            }
-
-            default: {
-                return getCallingPackage();
-            }
-        }
+        return switch (Binder.getCallingUid()) {
+            case Process.ROOT_UID -> "root";
+            case Process.SHELL_UID -> "com.android.shell";
+            default -> getCallingPackage();
+        };
     }
 
     private static final class Arguments {
@@ -2796,17 +2725,17 @@
         public Arguments(Uri uri, String where, String[] whereArgs, boolean supportAll) {
             final int segmentSize = uri.getPathSegments().size();
             switch (segmentSize) {
-                case 1: {
+                case 1 -> {
                     if (where != null
                             && (WHERE_PATTERN_WITH_PARAM_NO_BRACKETS.matcher(where).matches()
-                                || WHERE_PATTERN_WITH_PARAM_IN_BRACKETS.matcher(where).matches())
+                            || WHERE_PATTERN_WITH_PARAM_IN_BRACKETS.matcher(where).matches())
                             && whereArgs.length == 1) {
                         name = whereArgs[0];
                         table = computeTableForSetting(uri, name);
                         return;
                     } else if (where != null
                             && (WHERE_PATTERN_NO_PARAM_NO_BRACKETS.matcher(where).matches()
-                                || WHERE_PATTERN_NO_PARAM_IN_BRACKETS.matcher(where).matches())) {
+                            || WHERE_PATTERN_NO_PARAM_IN_BRACKETS.matcher(where).matches())) {
                         final int startIndex = Math.max(where.indexOf("'"),
                                 where.indexOf("\"")) + 1;
                         final int endIndex = Math.max(where.lastIndexOf("'"),
@@ -2819,15 +2748,14 @@
                         table = computeTableForSetting(uri, null);
                         return;
                     }
-                } break;
-
-                case 2: {
+                }
+                case 2 -> {
                     if (where == null && whereArgs == null) {
                         name = uri.getPathSegments().get(1);
                         table = computeTableForSetting(uri, name);
                         return;
                     }
-                } break;
+                }
             }
 
             EventLogTags.writeUnsupportedSettingsQuery(
@@ -2956,10 +2884,11 @@
 
         public SettingsRegistry() {
             mHandler = new MyHandler(getContext().getMainLooper());
-            mGenerationRegistry = new GenerationRegistry(mLock, UserManager.getMaxSupportedUsers());
+            mGenerationRegistry = new GenerationRegistry(UserManager.getMaxSupportedUsers());
             mBackupManager = new BackupManager(getContext());
         }
 
+        @GuardedBy("mLock")
         private void generateUserKeyLocked(int userId) {
             // Generate a random key for each user used for creating a new ssaid.
             final byte[] keyBytes = new byte[32];
@@ -2983,6 +2912,7 @@
             return ByteBuffer.allocate(4).putInt(data.length).array();
         }
 
+        @GuardedBy("mLock")
         public Setting generateSsaidLocked(PackageInfo callingPkg, int userId) {
             // Read the user's key from the ssaid table.
             Setting userKeySetting = getSettingLocked(SETTINGS_TYPE_SSAID, userId, SSAID_USER_KEY);
@@ -3044,6 +2974,7 @@
             return getSettingLocked(SETTINGS_TYPE_SSAID, userId, uid);
         }
 
+        @GuardedBy("mLock")
         private void syncSsaidTableOnStartLocked() {
             // Verify that each user's packages and ssaid's are in sync.
             for (UserInfo user : mUserManager.getAliveUsers()) {
@@ -3078,15 +3009,17 @@
             }
         }
 
+        @GuardedBy("mLock")
         public List<String> getSettingsNamesLocked(int type, int userId) {
             final int key = makeKey(type, userId);
-            SettingsState settingsState = peekSettingsStateLocked(key);
+            SettingsState settingsState = mSettingsStates.get(key);
             if (settingsState == null) {
                 return new ArrayList<>();
             }
             return settingsState.getSettingNamesLocked();
         }
 
+        @GuardedBy("mLock")
         public SparseBooleanArray getKnownUsersLocked() {
             SparseBooleanArray users = new SparseBooleanArray();
             for (int i = mSettingsStates.size()-1; i >= 0; i--) {
@@ -3095,17 +3028,19 @@
             return users;
         }
 
+        @GuardedBy("mLock")
         @Nullable
         public SettingsState getSettingsLocked(int type, int userId) {
             final int key = makeKey(type, userId);
-            return peekSettingsStateLocked(key);
+            return mSettingsStates.get(key);
         }
 
-        public boolean ensureSettingsForUserLocked(int userId) {
+        @GuardedBy("mLock")
+        public void ensureSettingsForUserLocked(int userId) {
             // First make sure this user actually exists.
             if (mUserManager.getUserInfo(userId) == null) {
                 Slog.wtf(LOG_TAG, "Requested user " + userId + " does not exist");
-                return false;
+                return;
             }
 
             // Migrate the setting for this user if needed.
@@ -3143,9 +3078,9 @@
             // Upgrade the settings to the latest version.
             UpgradeController upgrader = new UpgradeController(userId);
             upgrader.upgradeIfNeededLocked();
-            return true;
         }
 
+        @GuardedBy("mLock")
         private void ensureSettingsStateLocked(int key) {
             if (mSettingsStates.get(key) == null) {
                 final int maxBytesPerPackage = getMaxBytesPerPackageForType(getTypeFromKey(key));
@@ -3155,6 +3090,7 @@
             }
         }
 
+        @GuardedBy("mLock")
         public void removeUserStateLocked(int userId, boolean permanently) {
             // We always keep the global settings in memory.
 
@@ -3166,12 +3102,7 @@
                     mSettingsStates.remove(systemKey);
                     systemSettingsState.destroyLocked(null);
                 } else {
-                    systemSettingsState.destroyLocked(new Runnable() {
-                        @Override
-                        public void run() {
-                            mSettingsStates.remove(systemKey);
-                        }
-                    });
+                    systemSettingsState.destroyLocked(() -> mSettingsStates.remove(systemKey));
                 }
             }
 
@@ -3183,12 +3114,7 @@
                     mSettingsStates.remove(secureKey);
                     secureSettingsState.destroyLocked(null);
                 } else {
-                    secureSettingsState.destroyLocked(new Runnable() {
-                        @Override
-                        public void run() {
-                            mSettingsStates.remove(secureKey);
-                        }
-                    });
+                    secureSettingsState.destroyLocked(() -> mSettingsStates.remove(secureKey));
                 }
             }
 
@@ -3200,12 +3126,7 @@
                     mSettingsStates.remove(ssaidKey);
                     ssaidSettingsState.destroyLocked(null);
                 } else {
-                    ssaidSettingsState.destroyLocked(new Runnable() {
-                        @Override
-                        public void run() {
-                            mSettingsStates.remove(ssaidKey);
-                        }
-                    });
+                    ssaidSettingsState.destroyLocked(() -> mSettingsStates.remove(ssaidKey));
                 }
             }
 
@@ -3213,6 +3134,7 @@
             mGenerationRegistry.onUserRemoved(userId);
         }
 
+        @GuardedBy("mLock")
         public boolean insertSettingLocked(int type, int userId, String name, String value,
                 String tag, boolean makeDefault, String packageName, boolean forceNotify,
                 Set<String> criticalSettings, boolean overrideableByRestore) {
@@ -3220,6 +3142,7 @@
                     packageName, forceNotify, criticalSettings, overrideableByRestore);
         }
 
+        @GuardedBy("mLock")
         public boolean insertSettingLocked(int type, int userId, String name, String value,
                 String tag, boolean makeDefault, boolean forceNonSystemPackage, String packageName,
                 boolean forceNotify, Set<String> criticalSettings, boolean overrideableByRestore) {
@@ -3232,7 +3155,7 @@
 
             boolean success = false;
             boolean wasUnsetNonPredefinedSetting = false;
-            SettingsState settingsState = peekSettingsStateLocked(key);
+            SettingsState settingsState = mSettingsStates.get(key);
             if (settingsState != null) {
                 if (!isSettingPreDefined(name, type) && !settingsState.hasSetting(name)) {
                     wasUnsetNonPredefinedSetting = true;
@@ -3264,9 +3187,10 @@
          * Set Config Settings using consumed keyValues, returns true if the keyValues can be set,
          * false otherwise.
          */
+        @GuardedBy("mLock")
         public boolean setConfigSettingsLocked(int key, String prefix,
                 Map<String, String> keyValues, String packageName) {
-            SettingsState settingsState = peekSettingsStateLocked(key);
+            SettingsState settingsState = mSettingsStates.get(key);
             if (settingsState != null) {
                 if (settingsState.isNewConfigBannedLocked(prefix, keyValues)) {
                     return false;
@@ -3283,12 +3207,13 @@
             return true;
         }
 
+        @GuardedBy("mLock")
         public boolean deleteSettingLocked(int type, int userId, String name, boolean forceNotify,
                 Set<String> criticalSettings) {
             final int key = makeKey(type, userId);
 
             boolean success = false;
-            SettingsState settingsState = peekSettingsStateLocked(key);
+            SettingsState settingsState = mSettingsStates.get(key);
             if (settingsState != null) {
                 success = settingsState.deleteSettingLocked(name);
             }
@@ -3306,13 +3231,14 @@
             return success;
         }
 
+        @GuardedBy("mLock")
         public boolean updateSettingLocked(int type, int userId, String name, String value,
                 String tag, boolean makeDefault, String packageName, boolean forceNotify,
                 Set<String> criticalSettings) {
             final int key = makeKey(type, userId);
 
             boolean success = false;
-            SettingsState settingsState = peekSettingsStateLocked(key);
+            SettingsState settingsState = mSettingsStates.get(key);
             if (settingsState != null) {
                 success = settingsState.updateSettingLocked(name, value, tag,
                         makeDefault, packageName);
@@ -3331,10 +3257,11 @@
             return success;
         }
 
+        @GuardedBy("mLock")
         public Setting getSettingLocked(int type, int userId, String name) {
             final int key = makeKey(type, userId);
 
-            SettingsState settingsState = peekSettingsStateLocked(key);
+            SettingsState settingsState = mSettingsStates.get(key);
             if (settingsState == null) {
                 return null;
             }
@@ -3352,16 +3279,18 @@
             return Global.SECURE_FRP_MODE.equals(setting.getName());
         }
 
+        @GuardedBy("mLock")
         public boolean resetSettingsLocked(int type, int userId, String packageName, int mode,
                 String tag) {
             return resetSettingsLocked(type, userId, packageName, mode, tag, /*prefix=*/
                     null);
         }
 
+        @GuardedBy("mLock")
         public boolean resetSettingsLocked(int type, int userId, String packageName, int mode,
                 String tag, @Nullable String prefix) {
             final int key = makeKey(type, userId);
-            SettingsState settingsState = peekSettingsStateLocked(key);
+            SettingsState settingsState = mSettingsStates.get(key);
             if (settingsState == null) {
                 return false;
             }
@@ -3369,7 +3298,7 @@
             boolean success = false;
             banConfigurationIfNecessary(type, prefix, settingsState);
             switch (mode) {
-                case Settings.RESET_MODE_PACKAGE_DEFAULTS: {
+                case Settings.RESET_MODE_PACKAGE_DEFAULTS -> {
                     for (String name : settingsState.getSettingNamesLocked()) {
                         boolean someSettingChanged = false;
                         Setting setting = settingsState.getSettingLocked(name);
@@ -3389,9 +3318,8 @@
                             success = true;
                         }
                     }
-                } break;
-
-                case Settings.RESET_MODE_UNTRUSTED_DEFAULTS: {
+                }
+                case Settings.RESET_MODE_UNTRUSTED_DEFAULTS -> {
                     for (String name : settingsState.getSettingNamesLocked()) {
                         boolean someSettingChanged = false;
                         Setting setting = settingsState.getSettingLocked(name);
@@ -3411,9 +3339,8 @@
                             success = true;
                         }
                     }
-                } break;
-
-                case Settings.RESET_MODE_UNTRUSTED_CHANGES: {
+                }
+                case Settings.RESET_MODE_UNTRUSTED_CHANGES -> {
                     for (String name : settingsState.getSettingNamesLocked()) {
                         boolean someSettingChanged = false;
                         Setting setting = settingsState.getSettingLocked(name);
@@ -3439,9 +3366,8 @@
                             success = true;
                         }
                     }
-                } break;
-
-                case Settings.RESET_MODE_TRUSTED_DEFAULTS: {
+                }
+                case Settings.RESET_MODE_TRUSTED_DEFAULTS -> {
                     for (String name : settingsState.getSettingNamesLocked()) {
                         Setting setting = settingsState.getSettingLocked(name);
                         boolean someSettingChanged = false;
@@ -3464,11 +3390,12 @@
                             success = true;
                         }
                     }
-                } break;
+                }
             }
             return success;
         }
 
+        @GuardedBy("mLock")
         public void removeSettingsForPackageLocked(String packageName, int userId) {
             // Global and secure settings are signature protected. Apps signed
             // by the platform certificate are generally not uninstalled  and
@@ -3482,6 +3409,7 @@
             }
         }
 
+        @GuardedBy("mLock")
         public void onUidRemovedLocked(int uid) {
             final SettingsState ssaidSettings = getSettingsLocked(SETTINGS_TYPE_SSAID,
                     UserHandle.getUserId(uid));
@@ -3490,19 +3418,7 @@
             }
         }
 
-        @Nullable
-        private SettingsState peekSettingsStateLocked(int key) {
-            SettingsState settingsState = mSettingsStates.get(key);
-            if (settingsState != null) {
-                return settingsState;
-            }
-
-            if (!ensureSettingsForUserLocked(getUserIdFromKey(key))) {
-                return null;
-            }
-            return mSettingsStates.get(key);
-        }
-
+        @GuardedBy("mLock")
         private void migrateAllLegacySettingsIfNeededLocked() {
             final int key = makeKey(SETTINGS_TYPE_GLOBAL, UserHandle.USER_SYSTEM);
             File globalFile = getSettingsFile(key);
@@ -3538,6 +3454,7 @@
             }
         }
 
+        @GuardedBy("mLock")
         private void migrateLegacySettingsForUserIfNeededLocked(int userId) {
             // Every user has secure settings and if no file we need to migrate.
             final int secureKey = makeKey(SETTINGS_TYPE_SECURE, userId);
@@ -3552,6 +3469,7 @@
             migrateLegacySettingsForUserLocked(dbHelper, database, userId);
         }
 
+        @GuardedBy("mLock")
         private void migrateLegacySettingsForUserLocked(DatabaseHelper dbHelper,
                 SQLiteDatabase database, int userId) {
             // Move over the system settings.
@@ -3596,6 +3514,7 @@
             }
         }
 
+        @GuardedBy("mLock")
         private void migrateLegacySettingsLocked(SettingsState settingsState,
                 SQLiteDatabase database, String table) {
             SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
@@ -3630,7 +3549,7 @@
             }
         }
 
-        @GuardedBy("secureSettings.mLock")
+        @GuardedBy("mLock")
         private void ensureSecureSettingAndroidIdSetLocked(SettingsState secureSettings) {
             Setting value = secureSettings.getSettingLocked(Settings.Secure.ANDROID_ID);
 
@@ -3706,6 +3625,7 @@
                     name, type, changeType);
         }
 
+        @GuardedBy("mLock")
         private void notifyForConfigSettingsChangeLocked(int key, String prefix,
                 List<String> changedSettings) {
 
@@ -3787,30 +3707,18 @@
             }
         }
 
-        private File getSettingsFile(int key) {
-            if (isConfigSettingsKey(key)) {
-                final int userId = getUserIdFromKey(key);
-                return new File(Environment.getUserSystemDirectory(userId),
-                        SETTINGS_FILE_CONFIG);
-            } else if (isGlobalSettingsKey(key)) {
-                final int userId = getUserIdFromKey(key);
-                return new File(Environment.getUserSystemDirectory(userId),
-                        SETTINGS_FILE_GLOBAL);
-            } else if (isSystemSettingsKey(key)) {
-                final int userId = getUserIdFromKey(key);
-                return new File(Environment.getUserSystemDirectory(userId),
-                        SETTINGS_FILE_SYSTEM);
-            } else if (isSecureSettingsKey(key)) {
-                final int userId = getUserIdFromKey(key);
-                return new File(Environment.getUserSystemDirectory(userId),
-                        SETTINGS_FILE_SECURE);
-            } else if (isSsaidSettingsKey(key)) {
-                final int userId = getUserIdFromKey(key);
-                return new File(Environment.getUserSystemDirectory(userId),
-                        SETTINGS_FILE_SSAID);
-            } else {
-                throw new IllegalArgumentException("Invalid settings key:" + key);
-            }
+        private static File getSettingsFile(int key) {
+            final int userId = getUserIdFromKey(key);
+            final int type = getTypeFromKey(key);
+            final File userSystemDirectory = Environment.getUserSystemDirectory(userId);
+            return switch (type) {
+                case SETTINGS_TYPE_CONFIG -> new File(userSystemDirectory, SETTINGS_FILE_CONFIG);
+                case SETTINGS_TYPE_GLOBAL -> new File(userSystemDirectory, SETTINGS_FILE_GLOBAL);
+                case SETTINGS_TYPE_SYSTEM -> new File(userSystemDirectory, SETTINGS_FILE_SYSTEM);
+                case SETTINGS_TYPE_SECURE -> new File(userSystemDirectory, SETTINGS_FILE_SECURE);
+                case SETTINGS_TYPE_SSAID -> new File(userSystemDirectory, SETTINGS_FILE_SSAID);
+                default -> throw new IllegalArgumentException("Invalid settings key:" + key);
+            };
         }
 
         private Uri getNotificationUriFor(int key, String name) {
@@ -3833,14 +3741,11 @@
 
         private int getMaxBytesPerPackageForType(int type) {
             switch (type) {
-                case SETTINGS_TYPE_CONFIG:
-                case SETTINGS_TYPE_GLOBAL:
-                case SETTINGS_TYPE_SECURE:
-                case SETTINGS_TYPE_SSAID: {
+                case SETTINGS_TYPE_CONFIG, SETTINGS_TYPE_GLOBAL, SETTINGS_TYPE_SECURE,
+                        SETTINGS_TYPE_SSAID -> {
                     return SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED;
                 }
-
-                default: {
+                default -> {
                     return SettingsState.MAX_BYTES_PER_APP_PACKAGE_LIMITED;
                 }
             }
@@ -3857,7 +3762,7 @@
             @Override
             public void handleMessage(Message msg) {
                 switch (msg.what) {
-                    case MSG_NOTIFY_URI_CHANGED: {
+                    case MSG_NOTIFY_URI_CHANGED -> {
                         final int userId = msg.arg1;
                         Uri uri = (Uri) msg.obj;
                         try {
@@ -3868,12 +3773,11 @@
                         if (DEBUG) {
                             Slog.v(LOG_TAG, "Notifying for " + userId + ": " + uri);
                         }
-                    } break;
-
-                    case MSG_NOTIFY_DATA_CHANGED: {
+                    }
+                    case MSG_NOTIFY_DATA_CHANGED -> {
                         mBackupManager.dataChanged();
                         scheduleWriteFallbackFilesJob();
-                    } break;
+                    }
                 }
             }
         }
@@ -3887,6 +3791,7 @@
                 mUserId = userId;
             }
 
+            @GuardedBy("mLock")
             public void upgradeIfNeededLocked() {
                 // The version of all settings for a user is the same (all users have secure).
                 SettingsState secureSettings = getSettingsLocked(
@@ -3944,18 +3849,22 @@
                 systemSettings.setVersionLocked(newVersion);
             }
 
+            @GuardedBy("mLock")
             private SettingsState getGlobalSettingsLocked() {
                 return getSettingsLocked(SETTINGS_TYPE_GLOBAL, UserHandle.USER_SYSTEM);
             }
 
+            @GuardedBy("mLock")
             private SettingsState getSecureSettingsLocked(int userId) {
                 return getSettingsLocked(SETTINGS_TYPE_SECURE, userId);
             }
 
+            @GuardedBy("mLock")
             private SettingsState getSsaidSettingsLocked(int userId) {
                 return getSettingsLocked(SETTINGS_TYPE_SSAID, userId);
             }
 
+            @GuardedBy("mLock")
             private SettingsState getSystemSettingsLocked(int userId) {
                 return getSettingsLocked(SETTINGS_TYPE_SYSTEM, userId);
             }
@@ -5399,7 +5308,7 @@
                     // next version step.
                     // If this is a new profile, check if a secure setting exists for the
                     // owner of the profile and use that value for the work profile.
-                    int owningId = resolveOwningUserIdForSecureSettingLocked(userId,
+                    int owningId = resolveOwningUserIdForSecureSetting(userId,
                             NOTIFICATION_BUBBLES);
                     Setting previous = getGlobalSettingsLocked()
                             .getSettingLocked("notification_bubbles");
@@ -6068,18 +5977,22 @@
                 return currentVersion;
             }
 
+            @GuardedBy("mLock")
             private void initGlobalSettingsDefaultValLocked(String key, boolean val) {
                 initGlobalSettingsDefaultValLocked(key, val ? "1" : "0");
             }
 
+            @GuardedBy("mLock")
             private void initGlobalSettingsDefaultValLocked(String key, int val) {
                 initGlobalSettingsDefaultValLocked(key, String.valueOf(val));
             }
 
+            @GuardedBy("mLock")
             private void initGlobalSettingsDefaultValLocked(String key, long val) {
                 initGlobalSettingsDefaultValLocked(key, String.valueOf(val));
             }
 
+            @GuardedBy("mLock")
             private void initGlobalSettingsDefaultValLocked(String key, String val) {
                 final SettingsState globalSettings = getGlobalSettingsLocked();
                 Setting currentSetting = globalSettings.getSettingLocked(key);
@@ -6198,6 +6111,7 @@
             }
         }
 
+        @GuardedBy("mLock")
         private void ensureLegacyDefaultValueAndSystemSetUpdatedLocked(SettingsState settings,
                 int userId) {
             List<String> names = settings.getSettingNamesLocked();
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 7bca944..9dacade 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -904,6 +904,7 @@
                         Settings.Secure.EXTRA_AUTOMATIC_POWER_SAVE_MODE,
                         Settings.Secure.GAME_DASHBOARD_ALWAYS_ON,
                         Settings.Secure.HDMI_CEC_SET_MENU_LANGUAGE_DENYLIST,
+                        Settings.Secure.HIDE_PRIVATESPACE_ENTRY_POINT,
                         Settings.Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING,
                         Settings.Secure.LOCATION_COARSE_ACCURACY_M,
                         Settings.Secure.LOCATION_SHOW_SYSTEM_OPS,
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/GenerationRegistryTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/GenerationRegistryTest.java
index 12865f4..8029785 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/GenerationRegistryTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/GenerationRegistryTest.java
@@ -36,7 +36,7 @@
 public class GenerationRegistryTest {
     @Test
     public void testGenerationsWithRegularSetting() throws IOException {
-        final GenerationRegistry generationRegistry = new GenerationRegistry(new Object(), 2);
+        final GenerationRegistry generationRegistry = new GenerationRegistry(2);
         final int secureKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_SECURE, 0);
         final String testSecureSetting = "test_secure_setting";
         Bundle b = new Bundle();
@@ -93,7 +93,7 @@
 
     @Test
     public void testGenerationsWithConfigSetting() throws IOException {
-        final GenerationRegistry generationRegistry = new GenerationRegistry(new Object(), 1);
+        final GenerationRegistry generationRegistry = new GenerationRegistry(1);
         final String prefix = "test_namespace/";
         final int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0);
 
@@ -110,7 +110,7 @@
 
     @Test
     public void testMaxNumBackingStores() throws IOException {
-        final GenerationRegistry generationRegistry = new GenerationRegistry(new Object(), 2);
+        final GenerationRegistry generationRegistry = new GenerationRegistry(2);
         final String testSecureSetting = "test_secure_setting";
         Bundle b = new Bundle();
         for (int i = 0; i < generationRegistry.getMaxNumBackingStores(); i++) {
@@ -133,7 +133,7 @@
 
     @Test
     public void testMaxSizeBackingStore() throws IOException {
-        final GenerationRegistry generationRegistry = new GenerationRegistry(new Object(), 1);
+        final GenerationRegistry generationRegistry = new GenerationRegistry(1);
         final int secureKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_SECURE, 0);
         final String testSecureSetting = "test_secure_setting";
         Bundle b = new Bundle();
@@ -153,7 +153,7 @@
 
     @Test
     public void testUnsetSettings() throws IOException {
-        final GenerationRegistry generationRegistry = new GenerationRegistry(new Object(), 1);
+        final GenerationRegistry generationRegistry = new GenerationRegistry(1);
         final int secureKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_SECURE, 0);
         final String testSecureSetting = "test_secure_setting";
         Bundle b = new Bundle();
@@ -172,7 +172,7 @@
 
     @Test
     public void testGlobalSettings() throws IOException {
-        final GenerationRegistry generationRegistry = new GenerationRegistry(new Object(), 2);
+        final GenerationRegistry generationRegistry = new GenerationRegistry(2);
         final int globalKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_GLOBAL, 0);
         final String testGlobalSetting = "test_global_setting";
         final Bundle b = new Bundle();
@@ -190,11 +190,11 @@
 
     @Test
     public void testNumberOfBackingStores() {
-        GenerationRegistry generationRegistry = new GenerationRegistry(new Object(), 0);
+        GenerationRegistry generationRegistry = new GenerationRegistry(0);
         // Test that the capacity of the backing stores is always valid
         assertThat(generationRegistry.getMaxNumBackingStores()).isEqualTo(
                 GenerationRegistry.MIN_NUM_BACKING_STORE);
-        generationRegistry = new GenerationRegistry(new Object(), 100);
+        generationRegistry = new GenerationRegistry(100);
         // Test that the capacity of the backing stores is always valid
         assertThat(generationRegistry.getMaxNumBackingStores()).isEqualTo(
                 GenerationRegistry.MAX_NUM_BACKING_STORE);
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/InstallNonMarketAppsDeprecationTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/InstallNonMarketAppsDeprecationTest.java
index 2b33057..2984cd3 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/InstallNonMarketAppsDeprecationTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/InstallNonMarketAppsDeprecationTest.java
@@ -17,10 +17,12 @@
 package com.android.providers.settings;
 
 import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
+
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assume.assumeTrue;
 
+import android.annotation.Nullable;
 import android.content.Context;
 import android.content.pm.UserInfo;
 import android.os.Process;
@@ -42,6 +44,8 @@
 import java.io.InputStreamReader;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
+import java.util.function.Supplier;
 
 @LargeTest
 public class InstallNonMarketAppsDeprecationTest extends BaseSettingsProviderTest {
@@ -54,35 +58,42 @@
     private boolean mSystemSetUserRestriction;
     private List<Integer> mUsersAddedByTest;
 
-    private String waitTillValueChanges(String errorMessage, String oldValue) {
+    private static String waitTillValueChanges(String errorMessage, @Nullable String oldValue,
+                                        Supplier<String> action) {
         boolean interrupted = false;
         final long startTime = SystemClock.uptimeMillis();
-        String newValue = getSetting(SETTING_TYPE_SECURE, Settings.Secure.INSTALL_NON_MARKET_APPS);
-        while (newValue.equals(oldValue) && SystemClock.uptimeMillis() <= (startTime
+        String newValue = action.get();
+        while (Objects.equals(newValue, oldValue) && SystemClock.uptimeMillis() <= (startTime
                 + USER_RESTRICTION_CHANGE_TIMEOUT)) {
             try {
                 Thread.sleep(1000);
             } catch (InterruptedException exc) {
                 interrupted = true;
             }
-            newValue = getSetting(SETTING_TYPE_SECURE, Settings.Secure.INSTALL_NON_MARKET_APPS);
+            newValue = action.get();
         }
         if (interrupted) {
             Thread.currentThread().interrupt();
         }
-        assertFalse(errorMessage, oldValue.equals(newValue));
+        assertNotEquals(errorMessage, newValue, oldValue);
         return newValue;
     }
 
-    private String getSecureSettingForUserViaShell(int userId) throws IOException {
+    private String getSecureSettingForUserViaShell(int userId) {
         StringBuilder sb = new StringBuilder("settings get --user ");
         sb.append(userId + " secure ");
         sb.append(Settings.Secure.INSTALL_NON_MARKET_APPS);
         BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(
                 InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
                         sb.toString()).getFileDescriptor())));
-        String line = reader.readLine();
-        return line.trim();
+        String line;
+        try {
+            line = reader.readLine();
+        } catch (IOException e) {
+            return null;
+        }
+        final String result = line.trim();
+        return (result.isEmpty() || result.equals("null")) ? null : result;
     }
 
     @Override
@@ -121,7 +132,10 @@
 
         UserInfo newUser = mUm.createUser("TEST_USER", 0);
         mUsersAddedByTest.add(newUser.id);
-        String value = getSecureSettingForUserViaShell(newUser.id);
+        // wait till SettingsProvider reacts to the USER_ADDED event
+        String value = waitTillValueChanges(
+                "install_non_market_apps should be set for the new user", null,
+                () -> getSecureSettingForUserViaShell(newUser.id));
         assertEquals("install_non_market_apps should be 1 for a new user", value, "1");
     }
 
@@ -140,13 +154,15 @@
         mUm.setUserRestriction(UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES, !mHasUserRestriction);
         value = waitTillValueChanges(
                 "Changing user restriction did not change the value of install_non_market_apps",
-                value);
+                value,
+                () -> getSetting(SETTING_TYPE_SECURE, Settings.Secure.INSTALL_NON_MARKET_APPS));
         assertTrue("Invalid value", value.equals("1") || value.equals("0"));
 
         mUm.setUserRestriction(UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES, mHasUserRestriction);
         value = waitTillValueChanges(
                 "Changing user restriction did not change the value of install_non_market_apps",
-                value);
+                value,
+                () -> getSetting(SETTING_TYPE_SECURE, Settings.Secure.INSTALL_NON_MARKET_APPS));
         assertTrue("Invalid value", value.equals("1") || value.equals("0"));
     }
 
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index e6a82e8..17cc9f8 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -485,6 +485,8 @@
         "motion_tool_lib",
         "androidx.core_core-animation-testing-nodeps",
         "androidx.compose.ui_ui",
+        "flag-junit",
+        "platform-test-annotations",
     ],
 }
 
diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS
index 8f329b3..34545cf 100644
--- a/packages/SystemUI/OWNERS
+++ b/packages/SystemUI/OWNERS
@@ -36,6 +36,7 @@
 hwwang@google.com
 hyunyoungs@google.com
 ikateryna@google.com
+iyz@google.com
 jaggies@google.com
 jamesoleary@google.com
 jbolinger@google.com
@@ -54,10 +55,12 @@
 kozynski@google.com
 kprevas@google.com
 lusilva@google.com
+liuyining@google.com
 lynhan@google.com
 madym@google.com
 mankoff@google.com
 mateuszc@google.com
+matiashe@google.com
 mgalhardo@google.com
 michaelmikhil@google.com
 michschn@google.com
@@ -94,6 +97,7 @@
 tsuji@google.com
 twickham@google.com
 vadimt@google.com
+valiiftime@google.com
 vanjan@google.com
 victortulias@google.com
 winsonc@google.com
diff --git a/packages/SystemUI/aconfig/accessibility.aconfig b/packages/SystemUI/aconfig/accessibility.aconfig
index bcf1535..08ecf09b 100644
--- a/packages/SystemUI/aconfig/accessibility.aconfig
+++ b/packages/SystemUI/aconfig/accessibility.aconfig
@@ -8,3 +8,10 @@
     description: "Adjusts bounds to allow the floating menu to render on top of navigation bars."
     bug: "283768342"
 }
+
+flag {
+    name: "floating_menu_ime_displacement_animation"
+    namespace: "accessibility"
+    description: "Adds an animation for when the FAB is displaced by an IME becoming visible."
+    bug: "281150010"
+}
\ No newline at end of file
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 0567528..c26d5f5 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -23,6 +23,14 @@
 }
 
 flag {
+    name: "notifications_icon_container_refactor"
+    namespace: "systemui"
+    description: "Enables the refactored version of the notification icon container in StatusBar, "
+        "AOD, and the notification shelf. Should not bring any behavioral changes."
+    bug: "278765923"
+}
+
+flag {
     name: "notification_lifetime_extension_refactor"
     namespace: "systemui"
     description: "Enables moving notification lifetime extension management from SystemUI to "
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt
index 17c74ba..0e7694e 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt
@@ -315,7 +315,8 @@
                 }
             }
 
-            override fun shouldAnimateExit(): Boolean = isComposed.value
+            override fun shouldAnimateExit(): Boolean =
+                isComposed.value && composeViewRoot.isAttachedToWindow && composeViewRoot.isShown
 
             override fun onExitAnimationCancelled() {
                 isDialogShowing.value = false
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
index 689a0a2..eb71490 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
@@ -46,7 +46,6 @@
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.drawWithContent
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.layout.layout
@@ -69,7 +68,6 @@
 import com.android.compose.modifiers.background
 import com.android.compose.theme.LocalAndroidColorScheme
 import com.android.compose.theme.colorAttr
-import com.android.systemui.res.R
 import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.ui.compose.Icon
@@ -78,6 +76,7 @@
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsForegroundServicesButtonViewModel
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsSecurityButtonViewModel
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
+import com.android.systemui.res.R
 import kotlinx.coroutines.launch
 
 /** The Quick Settings footer actions row. */
@@ -89,8 +88,7 @@
 ) {
     val context = LocalContext.current
 
-    // Collect visibility and alphas as soon as we are composed, even when not visible.
-    val isVisible by viewModel.isVisible.collectAsState()
+    // Collect alphas as soon as we are composed, even when not visible.
     val alpha by viewModel.alpha.collectAsState()
     val backgroundAlpha = viewModel.backgroundAlpha.collectAsState()
 
@@ -142,11 +140,6 @@
         modifier
             .fillMaxWidth()
             .graphicsLayer { this.alpha = alpha }
-            .drawWithContent {
-                if (isVisible) {
-                    drawContent()
-                }
-            }
             .then(backgroundModifier)
             .padding(
                 top = dimensionResource(R.dimen.qs_footer_actions_top_padding),
@@ -328,7 +321,7 @@
         shape = CircleShape,
         color = colorAttr(R.attr.underSurface),
         contentColor = LocalAndroidColorScheme.current.onSurfaceVariant,
-        borderStroke = BorderStroke(1.dp, colorAttr(R.attr.onShadeActive)),
+        borderStroke = BorderStroke(1.dp, colorAttr(R.attr.shadeInactive)),
         modifier = modifier.padding(horizontal = 4.dp),
         onClick = onClick,
     ) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
similarity index 81%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
index 07ffd11..1620088 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
@@ -1,15 +1,17 @@
 /*
  * Copyright (C) 2017 The Android Open Source Project
  *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
- * except in compliance with the License. You may obtain a copy of the License at
+ * 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.statusbar.phone;
@@ -19,8 +21,6 @@
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
 
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -29,9 +29,9 @@
 import android.content.BroadcastReceiver;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper.RunWithLooper;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
@@ -43,12 +43,13 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatchers;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 import java.util.concurrent.atomic.AtomicBoolean;
 
-@RunWith(AndroidTestingRunner.class)
+@RunWith(AndroidJUnit4.class)
 @RunWithLooper
 @SmallTest
 public class SystemUIDialogTest extends SysuiTestCase {
@@ -76,12 +77,13 @@
 
         dialog.show();
         verify(mBroadcastDispatcher).registerReceiver(broadcastReceiverCaptor.capture(),
-                intentFilterCaptor.capture(), eq(null), any());
+                intentFilterCaptor.capture(), ArgumentMatchers.eq(null), ArgumentMatchers.any());
         assertTrue(intentFilterCaptor.getValue().hasAction(Intent.ACTION_SCREEN_OFF));
         assertTrue(intentFilterCaptor.getValue().hasAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
 
         dialog.dismiss();
-        verify(mBroadcastDispatcher).unregisterReceiver(eq(broadcastReceiverCaptor.getValue()));
+        verify(mBroadcastDispatcher).unregisterReceiver(
+                ArgumentMatchers.eq(broadcastReceiverCaptor.getValue()));
     }
 
 
@@ -90,11 +92,12 @@
         final SystemUIDialog dialog = new SystemUIDialog(mContext, 0, false);
 
         dialog.show();
-        verify(mBroadcastDispatcher, never()).registerReceiver(any(), any(), eq(null), any());
+        verify(mBroadcastDispatcher, never()).registerReceiver(ArgumentMatchers.any(),
+                ArgumentMatchers.any(), ArgumentMatchers.eq(null), ArgumentMatchers.any());
         assertTrue(dialog.isShowing());
 
         dialog.dismiss();
-        verify(mBroadcastDispatcher, never()).unregisterReceiver(any());
+        verify(mBroadcastDispatcher, never()).unregisterReceiver(ArgumentMatchers.any());
         assertFalse(dialog.isShowing());
     }
 
diff --git a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
index 8241070..ad9a775 100644
--- a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
+++ b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
@@ -115,8 +115,7 @@
             android:layout_marginStart="@dimen/dream_overlay_status_icon_margin"
             android:src="@drawable/dream_overlay_assistant_attention_indicator"
             android:visibility="gone"
-            android:contentDescription=
-                "@string/dream_overlay_status_bar_assistant_attention_indicator" />
+            android:contentDescription="@string/assistant_attention_content_description" />
 
     </LinearLayout>
 </com.android.systemui.dreams.DreamOverlayStatusBarView>
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index 05f4334..74d435d 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -248,4 +248,14 @@
 
     <!-- Tag set on the Compose implementation of the QS footer actions. -->
     <item type="id" name="tag_compose_qs_footer_actions" />
+
+    <!--
+    Ids for the device entry icon.
+        device_entry_icon_view: parent view of both device_entry_icon and device_entry_icon_bg
+        device_entry_icon_fg: fp/lock/unlock icon
+        device_entry_icon_bg: background protection behind the icon
+    -->
+    <item type="id" name="device_entry_icon_view" />
+    <item type="id" name="device_entry_icon_fg" />
+    <item type="id" name="device_entry_icon_bg" />
 </resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 9e2ebf6..4c41ca4 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -629,7 +629,7 @@
     <!-- QuickSettings: Bluetooth detail panel, text when there are no items [CHAR LIMIT=NONE] -->
     <string name="quick_settings_bluetooth_detail_empty_text">No paired devices available</string>
     <!-- QuickSettings: Bluetooth dialog subtitle [CHAR LIMIT=NONE]-->
-    <string name="quick_settings_bluetooth_tile_subtitle">Tap a device to connect</string>
+    <string name="quick_settings_bluetooth_tile_subtitle">Tap to connect or disconnect a device </string>
     <!-- QuickSettings: Bluetooth dialog pair new devices [CHAR LIMIT=NONE]-->
     <string name="pair_new_bluetooth_devices">Pair new device</string>
     <!-- QuickSettings: Bluetooth dialog see all devices [CHAR LIMIT=NONE]-->
@@ -3022,8 +3022,6 @@
     <string name="dream_overlay_status_bar_mic_off">Mic is off</string>
     <!-- Content description for the camera and mic off icon in the dream overlay status bar [CHAR LIMIT=NONE] -->
     <string name="dream_overlay_status_bar_camera_mic_off">Camera and mic are off</string>
-    <!-- Content description for the assistant attention indicator [CHAR LIMIT=NONE] -->
-    <string name="dream_overlay_status_bar_assistant_attention_indicator">Assistant is listening</string>
     <!-- Content description for the notifications indicator icon in the dream overlay status bar [CHAR LIMIT=NONE] -->
     <string name="dream_overlay_status_bar_notification_indicator">{count, plural,
     =1 {# notification}
@@ -3209,7 +3207,7 @@
     <string name="priority_mode_dream_overlay_content_description">Priority mode on</string>
 
     <!-- Content description for when assistant attention is active [CHAR LIMIT=NONE] -->
-    <string name="assistant_attention_content_description">Assistant attention on</string>
+    <string name="assistant_attention_content_description">User presence is detected</string>
 
     <!--- Content of toast triggered when the notes app entry point is triggered without setting a default notes app. [CHAR LIMIT=NONE] -->
     <string name="set_default_notes_app_toast_content">Set default notes app in Settings</string>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index 3c8301f..7bf3e8f 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -38,10 +38,10 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.systemui.Dumpable;
-import com.android.systemui.res.R;
+import com.android.systemui.common.ui.ConfigurationState;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.FeatureFlagsClassic;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
@@ -50,14 +50,22 @@
 import com.android.systemui.log.dagger.KeyguardClockLog;
 import com.android.systemui.plugins.ClockController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.res.R;
 import com.android.systemui.shared.clocks.ClockRegistry;
 import com.android.systemui.shared.regionsampling.RegionSampler;
 import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController;
 import com.android.systemui.statusbar.notification.AnimatableProperty;
 import com.android.systemui.statusbar.notification.PropertyAnimator;
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore;
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder;
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel;
+import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor;
 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
+import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.NotificationIconContainer;
+import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
+import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.util.ViewController;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.settings.SecureSettings;
@@ -68,6 +76,8 @@
 
 import javax.inject.Inject;
 
+import kotlinx.coroutines.DisposableHandle;
+
 /**
  * Injectable controller for {@link KeyguardClockSwitch}.
  */
@@ -84,6 +94,12 @@
     private final DumpManager mDumpManager;
     private final ClockEventController mClockEventController;
     private final LogBuffer mLogBuffer;
+    private final NotificationIconContainerAlwaysOnDisplayViewModel mAodIconsViewModel;
+    private final ConfigurationState mConfigurationState;
+    private final ConfigurationController mConfigurationController;
+    private final DozeParameters mDozeParameters;
+    private final ScreenOffAnimationController mScreenOffAnimationController;
+    private final AlwaysOnDisplayNotificationIconViewStore mAodIconViewStore;
     private FrameLayout mSmallClockFrame; // top aligned clock
     private FrameLayout mLargeClockFrame; // centered clock
 
@@ -107,10 +123,13 @@
     private boolean mShownOnSecondaryDisplay = false;
     private boolean mOnlyClock = false;
     private boolean mIsActiveDreamLockscreenHosted = false;
-    private FeatureFlags mFeatureFlags;
+    private final FeatureFlagsClassic mFeatureFlags;
     private KeyguardInteractor mKeyguardInteractor;
     private final DelayableExecutor mUiExecutor;
     private boolean mCanShowDoubleLineClock = true;
+    private DisposableHandle mAodIconsBindJob;
+    @Nullable private NotificationIconContainer mAodIconContainer;
+
     @VisibleForTesting
     final Consumer<Boolean> mIsActiveDreamLockscreenHostedCallback =
             (Boolean isLockscreenHosted) -> {
@@ -151,26 +170,38 @@
             KeyguardSliceViewController keyguardSliceViewController,
             NotificationIconAreaController notificationIconAreaController,
             LockscreenSmartspaceController smartspaceController,
+            ConfigurationController configurationController,
+            ScreenOffAnimationController screenOffAnimationController,
             KeyguardUnlockAnimationController keyguardUnlockAnimationController,
             SecureSettings secureSettings,
             @Main DelayableExecutor uiExecutor,
             DumpManager dumpManager,
             ClockEventController clockEventController,
             @KeyguardClockLog LogBuffer logBuffer,
+            NotificationIconContainerAlwaysOnDisplayViewModel aodIconsViewModel,
+            ConfigurationState configurationState,
+            DozeParameters dozeParameters,
+            AlwaysOnDisplayNotificationIconViewStore aodIconViewStore,
             KeyguardInteractor keyguardInteractor,
-            FeatureFlags featureFlags) {
+            FeatureFlagsClassic featureFlags) {
         super(keyguardClockSwitch);
         mStatusBarStateController = statusBarStateController;
         mClockRegistry = clockRegistry;
         mKeyguardSliceViewController = keyguardSliceViewController;
         mNotificationIconAreaController = notificationIconAreaController;
         mSmartspaceController = smartspaceController;
+        mConfigurationController = configurationController;
+        mScreenOffAnimationController = screenOffAnimationController;
         mSecureSettings = secureSettings;
         mUiExecutor = uiExecutor;
         mKeyguardUnlockAnimationController = keyguardUnlockAnimationController;
         mDumpManager = dumpManager;
         mClockEventController = clockEventController;
         mLogBuffer = logBuffer;
+        mAodIconsViewModel = aodIconsViewModel;
+        mConfigurationState = configurationState;
+        mDozeParameters = dozeParameters;
+        mAodIconViewStore = aodIconViewStore;
         mView.setLogBuffer(mLogBuffer);
         mFeatureFlags = featureFlags;
         mKeyguardInteractor = keyguardInteractor;
@@ -316,6 +347,8 @@
     int getNotificationIconAreaHeight() {
         if (mFeatureFlags.isEnabled(Flags.MIGRATE_KEYGUARD_STATUS_VIEW)) {
             return 0;
+        } else if (NotificationIconContainerRefactor.isEnabled()) {
+            return mAodIconContainer != null ? mAodIconContainer.getHeight() : 0;
         } else {
             return mNotificationIconAreaController.getHeight();
         }
@@ -533,7 +566,27 @@
             NotificationIconContainer nic = (NotificationIconContainer)
                     mView.findViewById(
                             com.android.systemui.res.R.id.left_aligned_notification_icon_container);
-            mNotificationIconAreaController.setupAodIcons(nic);
+            if (NotificationIconContainerRefactor.isEnabled()) {
+                if (mAodIconsBindJob != null) {
+                    mAodIconsBindJob.dispose();
+                }
+                if (nic != null) {
+                    nic.setOnLockScreen(true);
+                    mAodIconsBindJob = NotificationIconContainerViewBinder.bind(
+                        nic,
+                        mAodIconsViewModel,
+                        mConfigurationState,
+                        mConfigurationController,
+                        mDozeParameters,
+                        mFeatureFlags,
+                        mScreenOffAnimationController,
+                        mAodIconViewStore
+                    );
+                    mAodIconContainer = nic;
+                }
+            } else {
+                mNotificationIconAreaController.setupAodIcons(nic);
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
index 2e21255..3d8aaaf 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
@@ -65,15 +65,23 @@
     private boolean mPaused;
 
     private final OnEditorActionListener mOnEditorActionListener = (v, actionId, event) -> {
-        // Check if this was the result of hitting the enter key
+        // Check if this was the result of hitting the IME done action
         final boolean isSoftImeEvent = event == null
                 && (actionId == EditorInfo.IME_NULL
                 || actionId == EditorInfo.IME_ACTION_DONE
                 || actionId == EditorInfo.IME_ACTION_NEXT);
-        final boolean isKeyboardEnterKey = event != null
-                && KeyEvent.isConfirmKey(event.getKeyCode())
-                && event.getAction() == KeyEvent.ACTION_DOWN;
-        if (isSoftImeEvent || isKeyboardEnterKey) {
+        if (isSoftImeEvent) {
+            verifyPasswordAndUnlock();
+            return true;
+        }
+        return false;
+    };
+
+    private final View.OnKeyListener mKeyListener = (v, keyCode, keyEvent) -> {
+        final boolean isKeyboardEnterKey = keyEvent != null
+                && KeyEvent.isConfirmKey(keyCode)
+                && keyEvent.getAction() == KeyEvent.ACTION_UP;
+        if (isKeyboardEnterKey) {
             verifyPasswordAndUnlock();
             return true;
         }
@@ -140,15 +148,16 @@
                 | InputType.TYPE_TEXT_VARIATION_PASSWORD);
 
         mView.onDevicePostureChanged(mPostureController.getDevicePosture());
+
         mPostureController.addCallback(mPostureCallback);
 
         // Set selected property on so the view can send accessibility events.
         mPasswordEntry.setSelected(true);
         mPasswordEntry.setOnEditorActionListener(mOnEditorActionListener);
+        mPasswordEntry.setOnKeyListener(mKeyListener);
         mPasswordEntry.addTextChangedListener(mTextWatcher);
         // Poke the wakelock any time the text is selected or modified
         mPasswordEntry.setOnClickListener(v -> mKeyguardSecurityCallback.userActivity());
-
         mSwitchImeButton.setOnClickListener(v -> {
             mKeyguardSecurityCallback.userActivity(); // Leave the screen on a bit longer
             // Do not show auxiliary subtypes in password lock screen.
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java
index 681aa70..175544d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java
@@ -89,10 +89,7 @@
 
     @Override
     public boolean onKeyDown(int keyCode, KeyEvent event) {
-        if (KeyEvent.isConfirmKey(keyCode)) {
-            mOkButton.performClick();
-            return true;
-        } else if (keyCode == KeyEvent.KEYCODE_DEL) {
+        if (keyCode == KeyEvent.KEYCODE_DEL) {
             mDeleteButton.performClick();
             return true;
         }
@@ -110,6 +107,15 @@
     }
 
     @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        if (KeyEvent.isConfirmKey(keyCode)) {
+            mOkButton.performClick();
+            return true;
+        }
+        return super.onKeyUp(keyCode, event);
+    }
+
+    @Override
     protected int getPromptReasonStringRes(int reason) {
         switch (reason) {
             case PROMPT_REASON_RESTART:
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
index b7d1171..376933d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
@@ -41,6 +41,9 @@
         if (event.getAction() == KeyEvent.ACTION_DOWN) {
             return mView.onKeyDown(keyCode, event);
         }
+        if (event.getAction() == KeyEvent.ACTION_UP) {
+            return mView.onKeyUp(keyCode, event);
+        }
         return false;
     };
 
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
index 584357b..0bd4859 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
@@ -1461,8 +1461,7 @@
         mDragView.setColorFilter(filter);
     }
 
-    @VisibleForTesting
-    void setBounceEffectDuration(int duration) {
+    private void setBounceEffectDuration(int duration) {
         mBounceEffectDuration = duration;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
index 105de16c..cd8bef1 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
@@ -38,6 +38,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
+import com.android.systemui.Flags;
 
 import java.util.HashMap;
 
@@ -91,16 +92,48 @@
     }
 
     void moveToPosition(PointF position) {
-        moveToPositionX(position.x);
-        moveToPositionY(position.y);
+        moveToPosition(position, /* animateMovement = */ false);
+    }
+
+    /* Moves position without updating underlying percentage position. Can be animated. */
+    void moveToPosition(PointF position, boolean animateMovement) {
+        if (Flags.floatingMenuImeDisplacementAnimation()) {
+            moveToPositionX(position.x, animateMovement);
+            moveToPositionY(position.y, animateMovement);
+        } else {
+            moveToPositionX(position.x, /* animateMovement = */ false);
+            moveToPositionY(position.y, /* animateMovement = */ false);
+        }
     }
 
     void moveToPositionX(float positionX) {
-        DynamicAnimation.TRANSLATION_X.setValue(mMenuView, positionX);
+        moveToPositionX(positionX, /* animateMovement = */ false);
     }
 
-    private void moveToPositionY(float positionY) {
-        DynamicAnimation.TRANSLATION_Y.setValue(mMenuView, positionY);
+    void moveToPositionX(float positionX, boolean animateMovement) {
+        if (animateMovement && Flags.floatingMenuImeDisplacementAnimation()) {
+            springMenuWith(DynamicAnimation.TRANSLATION_X,
+                    createSpringForce(),
+                    /* velocity = */ 0,
+                    positionX, /* writeToPosition = */ false);
+        } else {
+            DynamicAnimation.TRANSLATION_X.setValue(mMenuView, positionX);
+        }
+    }
+
+    void moveToPositionY(float positionY) {
+        moveToPositionY(positionY, /* animateMovement = */ false);
+    }
+
+    void moveToPositionY(float positionY, boolean animateMovement) {
+        if (animateMovement && Flags.floatingMenuImeDisplacementAnimation()) {
+            springMenuWith(DynamicAnimation.TRANSLATION_Y,
+                    createSpringForce(),
+                    /* velocity = */ 0,
+                    positionY, /* writeToPosition = */ false);
+        } else {
+            DynamicAnimation.TRANSLATION_Y.setValue(mMenuView, positionY);
+        }
     }
 
     void moveToPositionYIfNeeded(float positionY) {
@@ -151,7 +184,7 @@
     void moveAndPersistPosition(PointF position) {
         moveToPosition(position);
         mMenuView.onBoundsInParentChanged((int) position.x, (int) position.y);
-        constrainPositionAndUpdate(position);
+        constrainPositionAndUpdate(position, /* writeToPosition = */ true);
     }
 
     void removeMenu() {
@@ -180,17 +213,13 @@
         flingThenSpringMenuWith(DynamicAnimation.TRANSLATION_X,
                 startXVelocity,
                 FLING_FRICTION_SCALAR,
-                new SpringForce()
-                        .setStiffness(SPRING_STIFFNESS)
-                        .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
+                createSpringForce(),
                 finalPositionX);
 
         flingThenSpringMenuWith(DynamicAnimation.TRANSLATION_Y,
                 velocityY,
                 FLING_FRICTION_SCALAR,
-                new SpringForce()
-                        .setStiffness(SPRING_STIFFNESS)
-                        .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
+                createSpringForce(),
                 /* finalPosition= */ null);
     }
 
@@ -226,7 +255,8 @@
                     final float endPosition = finalPosition != null
                             ? finalPosition
                             : Math.max(min, Math.min(max, endValue));
-                    springMenuWith(property, spring, endVelocity, endPosition);
+                    springMenuWith(property, spring, endVelocity, endPosition,
+                            /* writeToPosition = */ true);
                 });
 
         cancelAnimation(property);
@@ -242,7 +272,7 @@
 
     @VisibleForTesting
     void springMenuWith(DynamicAnimation.ViewProperty property, SpringForce spring,
-            float velocity, float finalPosition) {
+            float velocity, float finalPosition, boolean writeToPosition) {
         final MenuPositionProperty menuPositionProperty = new MenuPositionProperty(property);
         final SpringAnimation springAnimation =
                 new SpringAnimation(mMenuView, menuPositionProperty)
@@ -257,7 +287,7 @@
                                             DynamicAnimation::isRunning);
                             if (!areAnimationsRunning) {
                                 onSpringAnimationsEnd(new PointF(mMenuView.getTranslationX(),
-                                        mMenuView.getTranslationY()));
+                                        mMenuView.getTranslationY()), writeToPosition);
                             }
                         })
                         .setStartVelocity(velocity);
@@ -281,7 +311,8 @@
         if (currentXTranslation < draggableBounds.left
                 || currentXTranslation > draggableBounds.right) {
             constrainPositionAndUpdate(
-                    new PointF(mMenuView.getTranslationX(), mMenuView.getTranslationY()));
+                    new PointF(mMenuView.getTranslationX(), mMenuView.getTranslationY()),
+                    /* writeToPosition = */ true);
             moveToEdgeAndHide();
             return true;
         }
@@ -298,15 +329,19 @@
         return mMenuView.isMoveToTucked();
     }
 
-    void moveToEdgeAndHide() {
-        mMenuView.updateMenuMoveToTucked(/* isMoveToTucked= */ true);
-
+    PointF getTuckedMenuPosition() {
         final PointF position = mMenuView.getMenuPosition();
         final float menuHalfWidth = mMenuView.getMenuWidth() / 2.0f;
         final float endX = isOnLeftSide()
                 ? position.x - menuHalfWidth
                 : position.x + menuHalfWidth;
-        moveToPosition(new PointF(endX, position.y));
+        return new PointF(endX, position.y);
+    }
+
+    void moveToEdgeAndHide() {
+        mMenuView.updateMenuMoveToTucked(/* isMoveToTucked= */ true);
+        final PointF position = mMenuView.getMenuPosition();
+        moveToPosition(getTuckedMenuPosition());
 
         // Keep the touch region let users could click extra space to pop up the menu view
         // from the screen edge
@@ -335,6 +370,11 @@
         mPositionAnimations.get(property).cancel();
     }
 
+    @VisibleForTesting
+    DynamicAnimation getAnimation(DynamicAnimation.ViewProperty property) {
+        return mPositionAnimations.getOrDefault(property, null);
+    }
+
     void onDraggingStart() {
         mMenuView.onDraggingStart();
     }
@@ -361,9 +401,9 @@
                 .start();
     }
 
-    private void onSpringAnimationsEnd(PointF position) {
+    private void onSpringAnimationsEnd(PointF position, boolean writeToPosition) {
         mMenuView.onBoundsInParentChanged((int) position.x, (int) position.y);
-        constrainPositionAndUpdate(position);
+        constrainPositionAndUpdate(position, writeToPosition);
 
         fadeOutIfEnabled();
 
@@ -372,7 +412,7 @@
         }
     }
 
-    private void constrainPositionAndUpdate(PointF position) {
+    private void constrainPositionAndUpdate(PointF position, boolean writeToPosition) {
         final Rect draggableBounds = mMenuView.getMenuDraggableBoundsExcludeIme();
         // Have the space gap margin between the top bound and the menu view, so actually the
         // position y range needs to cut the margin.
@@ -384,7 +424,12 @@
         final float percentageY = position.y < 0 || draggableBounds.height() == 0
                 ? MIN_PERCENT
                 : Math.min(MAX_PERCENT, position.y / draggableBounds.height());
-        mMenuView.persistPositionAndUpdateEdge(new Position(percentageX, percentageY));
+
+        if (Flags.floatingMenuImeDisplacementAnimation() && !writeToPosition) {
+            mMenuView.onEdgeChangedIfNeeded();
+        } else {
+            mMenuView.persistPositionAndUpdateEdge(new Position(percentageX, percentageY));
+        }
     }
 
     void updateOpacityWith(boolean isFadeEffectEnabled, float newOpacityValue) {
@@ -463,4 +508,11 @@
             mProperty.setValue(menuView, value);
         }
     }
+
+    @VisibleForTesting
+    static SpringForce createSpringForce() {
+        return new SpringForce()
+                .setStiffness(SPRING_STIFFNESS)
+                .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
index e1612b0..ea5a56c 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
@@ -54,7 +54,6 @@
     private final List<AccessibilityTarget> mTargetFeatures = new ArrayList<>();
     private final AccessibilityTargetAdapter mAdapter;
     private final MenuViewModel mMenuViewModel;
-    private final MenuAnimationController mMenuAnimationController;
     private final Rect mBoundsInParent = new Rect();
     private final RecyclerView mTargetFeaturesView;
     private final ViewTreeObserver.OnDrawListener mSystemGestureExcludeUpdater =
@@ -70,6 +69,7 @@
 
     private boolean mIsMoveToTucked;
 
+    private final MenuAnimationController mMenuAnimationController;
     private OnTargetFeaturesChangeListener mFeaturesChangeListener;
 
     MenuView(Context context, MenuViewModel menuViewModel, MenuViewAppearance menuViewAppearance) {
@@ -197,8 +197,30 @@
     }
 
     void onPositionChanged() {
-        final PointF position = mMenuViewAppearance.getMenuPosition();
-        mMenuAnimationController.moveToPosition(position);
+        onPositionChanged(/* animateMovement = */ false);
+    }
+
+    void onPositionChanged(boolean animateMovement) {
+        final PointF position;
+        if (isMoveToTucked()) {
+            position = mMenuAnimationController.getTuckedMenuPosition();
+        } else {
+            position = getMenuPosition();
+        }
+
+        // We can skip animating if FAB is not visible
+        if (Flags.floatingMenuImeDisplacementAnimation()
+                && animateMovement && getVisibility() == VISIBLE) {
+            mMenuAnimationController.moveToPosition(position, /* animateMovement = */ true);
+            // onArrivalAtPosition() is called at the end of the animation.
+        } else {
+            mMenuAnimationController.moveToPosition(position);
+            onArrivalAtPosition(); // no animation, so we call this immediately.
+        }
+    }
+
+    void onArrivalAtPosition() {
+        final PointF position = getMenuPosition();
         onBoundsInParentChanged((int) position.x, (int) position.y);
 
         if (isMoveToTucked()) {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
index b6e8997..fbca022 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
@@ -59,6 +59,7 @@
 import com.android.internal.accessibility.dialog.AccessibilityTarget;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
+import com.android.systemui.Flags;
 import com.android.systemui.res.R;
 import com.android.systemui.util.settings.SecureSettings;
 import com.android.wm.shell.bubbles.DismissViewUtils;
@@ -331,7 +332,7 @@
             mMenuViewAppearance.onImeVisibilityChanged(windowInsets.isVisible(ime()), imeTop);
 
             mMenuView.onEdgeChanged();
-            mMenuView.onPositionChanged();
+            mMenuView.onPositionChanged(/* animateMovement = */ true);
 
             mImeInsetsRect.set(imeInsetsRect);
         }
@@ -362,6 +363,10 @@
 
             mMenuAnimationController.startTuckedAnimationPreview();
         }
+
+        if (Flags.floatingMenuImeDisplacementAnimation()) {
+            mMenuView.onArrivalAtPosition();
+        }
     }
 
     private CharSequence getMigrationMessage() {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index 272e0f2..934f9f9 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -46,7 +46,6 @@
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.animation.ActivityLaunchAnimator
 import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
-import com.android.systemui.biometrics.ui.controller.UdfpsKeyguardViewController
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.dump.DumpManager
@@ -240,15 +239,14 @@
             }
             REASON_AUTH_KEYGUARD -> {
                 if (featureFlags.isEnabled(REFACTOR_UDFPS_KEYGUARD_VIEWS)) {
-                    udfpsKeyguardViewModels.get().setSensorBounds(sensorBounds)
-                    UdfpsKeyguardViewController(
-                        view.addUdfpsView(R.layout.udfps_keyguard_view),
-                        statusBarStateController,
-                        primaryBouncerInteractor,
-                        dialogManager,
-                        dumpManager,
-                        alternateBouncerInteractor,
-                        udfpsKeyguardViewModels.get(),
+                    // note: empty controller, currently shows no visual affordance
+                    // instead SysUI will show the fingerprint icon in its DeviceEntryIconView
+                    UdfpsBpViewController(
+                            view.addUdfpsView(R.layout.udfps_bp_view),
+                            statusBarStateController,
+                            primaryBouncerInteractor,
+                            dialogManager,
+                            dumpManager
                     )
                 } else {
                     UdfpsKeyguardViewControllerLegacy(
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt
index 56e3f39..2f493ac 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt
@@ -2,11 +2,11 @@
 
 import android.content.Context
 import android.content.res.Configuration.ORIENTATION_LANDSCAPE
+import android.graphics.Insets
 import android.text.TextUtils
 import android.util.AttributeSet
 import android.view.View
 import android.view.WindowInsets
-import android.view.WindowInsets.Type.ime
 import android.view.accessibility.AccessibilityManager
 import android.widget.LinearLayout
 import android.widget.TextView
@@ -41,7 +41,10 @@
     }
 
     override fun onApplyWindowInsets(v: View, insets: WindowInsets): WindowInsets {
-        val imeBottomInset = insets.getInsets(ime()).bottom
+        val statusBarInsets: Insets = insets.getInsets(WindowInsets.Type.statusBars())
+        val keyboardInsets: Insets = insets.getInsets(WindowInsets.Type.ime())
+        val navigationInsets: Insets = insets.getInsets(WindowInsets.Type.navigationBars())
+        val imeBottomInset = keyboardInsets.bottom
         if (bottomInset != imeBottomInset) {
             val titleView: TextView? = findViewById(R.id.title)
             if (titleView != null) {
@@ -61,8 +64,14 @@
                 }
             }
         }
-        setPadding(paddingLeft, paddingTop, paddingRight, imeBottomInset)
-        return insets.inset(0, 0, 0, imeBottomInset)
+
+        setPadding(
+            0,
+            statusBarInsets.top,
+            0,
+            if (keyboardInsets.bottom == 0) navigationInsets.bottom else keyboardInsets.bottom
+        )
+        return WindowInsets.CONSUMED
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt
index 75331f0..1086897 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt
@@ -1,7 +1,11 @@
 package com.android.systemui.biometrics.ui
 
 import android.content.Context
+import android.graphics.Insets
 import android.util.AttributeSet
+import android.view.View
+import android.view.WindowInsets
+import android.view.WindowInsets.Type
 import android.widget.LinearLayout
 import com.android.systemui.biometrics.AuthPanelController
 import com.android.systemui.biometrics.ui.binder.CredentialViewBinder
@@ -9,7 +13,7 @@
 
 /** Pattern credential view for BiometricPrompt. */
 class CredentialPatternView(context: Context, attrs: AttributeSet?) :
-    LinearLayout(context, attrs), CredentialView {
+    LinearLayout(context, attrs), CredentialView, View.OnApplyWindowInsetsListener {
 
     /** Initializes the view. */
     override fun init(
@@ -20,4 +24,17 @@
     ) {
         CredentialViewBinder.bind(this, host, viewModel, panelViewController, animatePanel)
     }
+
+    override fun onFinishInflate() {
+        super.onFinishInflate()
+        setOnApplyWindowInsetsListener(this)
+    }
+
+    override fun onApplyWindowInsets(v: View, insets: WindowInsets): WindowInsets {
+        val statusBarInsets: Insets = insets.getInsets(Type.statusBars())
+        val navigationInsets: Insets = insets.getInsets(Type.navigationBars())
+
+        setPadding(0, statusBarInsets.top, 0, navigationInsets.bottom)
+        return WindowInsets.CONSUMED
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt
index 8d5b84f..7bca86e 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt
@@ -15,18 +15,26 @@
 package com.android.systemui.common.ui
 
 import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
 import androidx.annotation.AttrRes
 import androidx.annotation.ColorInt
 import androidx.annotation.DimenRes
+import androidx.annotation.LayoutRes
 import com.android.settingslib.Utils
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.onDensityOrFontScaleChanged
 import com.android.systemui.statusbar.policy.onThemeChanged
 import com.android.systemui.util.kotlin.emitOnStart
+import com.android.systemui.util.view.bindLatest
 import javax.inject.Inject
+import kotlinx.coroutines.DisposableHandle
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
 
 /** Configuration-aware-state-tracking utilities. */
 class ConfigurationState
@@ -34,6 +42,7 @@
 constructor(
     private val configurationController: ConfigurationController,
     @Application private val context: Context,
+    private val layoutInflater: LayoutInflater,
 ) {
     /**
      * Returns a [Flow] that emits a dimension pixel size that is kept in sync with the device
@@ -57,4 +66,65 @@
             Utils.getColorAttrDefaultColor(context, id, defaultValue)
         }
     }
+
+    /**
+     * Returns a [Flow] that emits a [View] that is re-inflated as necessary to remain in sync with
+     * the device configuration.
+     *
+     * @see LayoutInflater.inflate
+     */
+    @Suppress("UNCHECKED_CAST")
+    fun <T : View> inflateLayout(
+        @LayoutRes id: Int,
+        root: ViewGroup?,
+        attachToRoot: Boolean,
+    ): Flow<T> {
+        // TODO(b/305930747): This may lead to duplicate invocations if both flows emit, find a
+        //  solution to only emit one event.
+        return merge(
+                configurationController.onThemeChanged,
+                configurationController.onDensityOrFontScaleChanged,
+            )
+            .emitOnStart()
+            .map { layoutInflater.inflate(id, root, attachToRoot) as T }
+    }
+}
+
+/**
+ * Perform an inflation right away, then re-inflate whenever the device configuration changes, and
+ * call [onInflate] on the resulting view each time. Disposes of the [DisposableHandle] returned by
+ * [onInflate] when done.
+ *
+ * This never completes unless cancelled, it just suspends and waits for updates.
+ *
+ * For parameters [resource], [root] and [attachToRoot], see [LayoutInflater.inflate].
+ *
+ * An example use-case of this is when a view needs to be re-inflated whenever a configuration
+ * change occurs, which would require the ViewBinder to then re-bind the new view. For example, the
+ * code in the parent view's binder would look like:
+ * ```
+ * parentView.repeatWhenAttached {
+ *     configurationState
+ *         .reinflateOnChange(
+ *             R.layout.my_layout,
+ *             parentView,
+ *             attachToRoot = false,
+ *             coroutineScope = lifecycleScope,
+ *             configurationController.onThemeChanged,
+ *         ) { view: ChildView ->
+ *             ChildViewBinder.bind(view, childViewModel)
+ *         }
+ * }
+ * ```
+ *
+ * In turn, the bind method (passed through [onInflate]) uses [repeatWhenAttached], which returns a
+ * [DisposableHandle].
+ */
+suspend fun <T : View> ConfigurationState.reinflateAndBindLatest(
+    @LayoutRes resource: Int,
+    root: ViewGroup?,
+    attachToRoot: Boolean,
+    onInflate: (T) -> DisposableHandle?,
+) {
+    inflateLayout<T>(resource, root, attachToRoot).bindLatest(onInflate)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt
index 323070a..0781451 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt
@@ -22,6 +22,7 @@
 import android.util.AttributeSet
 import android.view.MotionEvent
 import android.view.View
+import android.view.ViewConfiguration
 import com.android.systemui.shade.TouchLogger
 import kotlin.math.pow
 import kotlin.math.sqrt
@@ -36,11 +37,18 @@
 class LongPressHandlingView(
     context: Context,
     attrs: AttributeSet?,
+    private val longPressDuration: () -> Long,
 ) :
     View(
         context,
         attrs,
     ) {
+
+    constructor(
+        context: Context,
+        attrs: AttributeSet?,
+    ) : this(context, attrs, { ViewConfiguration.getLongPressTimeout().toLong() })
+
     interface Listener {
         /** Notifies that a long-press has been detected by the given view. */
         fun onLongPressDetected(
@@ -77,6 +85,7 @@
                 )
             },
             onSingleTapDetected = { listener?.onSingleTapDetected(this@LongPressHandlingView) },
+            longPressDuration = longPressDuration,
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt
index c2d4d12..a742e8d 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt
@@ -33,6 +33,8 @@
     private val onLongPressDetected: (x: Int, y: Int) -> Unit,
     /** Callback reporting the a single tap gesture was detected at the given coordinates. */
     private val onSingleTapDetected: () -> Unit,
+    /** Time for the touch to be considered a long-press in ms */
+    private val longPressDuration: () -> Long,
 ) {
     sealed class MotionEventModel {
         object Other : MotionEventModel()
@@ -77,7 +79,7 @@
                 cancelScheduledLongPress()
                 if (
                     event.distanceMoved <= ViewConfiguration.getTouchSlop() &&
-                        event.gestureDuration < ViewConfiguration.getLongPressTimeout()
+                        event.gestureDuration < longPressDuration()
                 ) {
                     dispatchSingleTap()
                 }
@@ -103,7 +105,7 @@
                         y = y,
                     )
                 },
-                ViewConfiguration.getLongPressTimeout().toLong(),
+                longPressDuration(),
             )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 8c81fbb..28ba5f3 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -87,11 +87,6 @@
     @JvmField
     val NOTIFICATION_SHELF_REFACTOR = releasedFlag("notification_shelf_refactor")
 
-    // TODO(b/290787599): Tracking Bug
-    @JvmField
-    val NOTIFICATION_ICON_CONTAINER_REFACTOR =
-        unreleasedFlag("notification_icon_container_refactor")
-
     // TODO(b/288326013): Tracking Bug
     @JvmField
     val NOTIFICATION_ASYNC_HYBRID_VIEW_INFLATION =
@@ -123,7 +118,7 @@
     // TODO(b/301955929)
     @JvmField
     val NOTIF_LS_BACKGROUND_THREAD =
-            unreleasedFlag("notification_lockscreen_mgr_bg_thread")
+            unreleasedFlag("notification_lockscreen_mgr_bg_thread", teamfood = true)
 
     // 200 - keyguard/lockscreen
     // ** Flag retired **
@@ -484,7 +479,7 @@
     val MEDIA_REMOTE_RESUME = unreleasedFlag("media_remote_resume")
 
     // TODO(b/304506662): Tracking Bug
-    val MEDIA_DEVICE_NAME_FIX = unreleasedFlag("media_device_name_fix", teamfood = true)
+    val MEDIA_DEVICE_NAME_FIX = releasedFlag("media_device_name_fix")
 
     // 1000 - dock
     val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag("simulate_dock_through_charging")
@@ -623,7 +618,7 @@
 
     /** TODO(b/295143676): Tracking bug. When enable, captures a screenshot for each display. */
     @JvmField
-    val MULTI_DISPLAY_SCREENSHOT = unreleasedFlag("multi_display_screenshot", teamfood = true)
+    val MULTI_DISPLAY_SCREENSHOT = releasedFlag("multi_display_screenshot")
 
     // 1400 - columbus
     // TODO(b/254512756): Tracking Bug
@@ -727,7 +722,7 @@
     @JvmField val KEYBOARD_BACKLIGHT_INDICATOR = releasedFlag("keyboard_backlight_indicator")
 
     // TODO(b/277192623): Tracking Bug
-    @JvmField val KEYBOARD_EDUCATION = unreleasedFlag("keyboard_education", teamfood = true)
+    @JvmField val KEYBOARD_EDUCATION = releasedFlag("keyboard_education")
 
     // TODO(b/277201412): Tracking Bug
     @JvmField
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index 119ade4..c56dfde 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -48,6 +48,7 @@
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
+import dagger.Lazy
 import javax.inject.Inject
 import kotlinx.coroutines.DisposableHandle
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -72,7 +73,7 @@
     private val keyguardStatusViewComponentFactory: KeyguardStatusViewComponent.Factory,
     private val context: Context,
     private val keyguardIndicationController: KeyguardIndicationController,
-    private val lockIconViewController: LockIconViewController,
+    private val lockIconViewController: Lazy<LockIconViewController>,
     private val shadeInteractor: ShadeInteractor,
     private val interactionJankMonitor: InteractionJankMonitor,
     private val deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor,
@@ -131,7 +132,9 @@
         val indicationArea = KeyguardIndicationArea(context, null)
         keyguardIndicationController.setIndicationArea(indicationArea)
 
-        lockIconViewController.setLockIconView(LockIconView(context, null))
+        if (!featureFlags.isEnabled(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS)) {
+            lockIconViewController.get().setLockIconView(LockIconView(context, null))
+        }
     }
 
     private fun bindKeyguardRootView() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index 2dc4908..4d26466 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -93,6 +93,12 @@
     val isKeyguardOccluded: Flow<Boolean>
 
     /**
+     * Whether the device is locked or unlocked right now. This is true when keyguard has been
+     * dismissed or can be dismissed by a swipe
+     */
+    val isKeyguardUnlocked: StateFlow<Boolean>
+
+    /**
      * Observable for the signal that keyguard is about to go away.
      *
      * TODO(b/278086361): Remove once KEYGUARD_WM_STATE_REFACTOR flag is removed.
@@ -332,6 +338,44 @@
             }
             .distinctUntilChanged()
 
+    override val isKeyguardUnlocked: StateFlow<Boolean> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : KeyguardStateController.Callback {
+                        override fun onUnlockedChanged() {
+                            trySendWithFailureLogging(
+                                keyguardStateController.isUnlocked,
+                                TAG,
+                                "updated isKeyguardUnlocked due to onUnlockedChanged"
+                            )
+                        }
+
+                        override fun onKeyguardShowingChanged() {
+                            trySendWithFailureLogging(
+                                keyguardStateController.isUnlocked,
+                                TAG,
+                                "updated isKeyguardUnlocked due to onKeyguardShowingChanged"
+                            )
+                        }
+                    }
+
+                keyguardStateController.addCallback(callback)
+                // Adding the callback does not send an initial update.
+                trySendWithFailureLogging(
+                    keyguardStateController.isUnlocked,
+                    TAG,
+                    "initial isKeyguardUnlocked"
+                )
+
+                awaitClose { keyguardStateController.removeCallback(callback) }
+            }
+            .distinctUntilChanged()
+            .stateIn(
+                scope,
+                SharingStarted.Eagerly,
+                initialValue = false,
+            )
+
     override val isKeyguardGoingAway: Flow<Boolean> = conflatedCallbackFlow {
         val callback =
             object : KeyguardStateController.Callback {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index b953b48..eaec0d4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -30,7 +30,6 @@
 import com.android.systemui.common.shared.model.SharedNotificationContainerPosition
 import com.android.systemui.common.ui.data.repository.ConfigurationRepository
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.repository.KeyguardRepository
@@ -78,7 +77,6 @@
     private val powerInteractor: PowerInteractor,
     featureFlags: FeatureFlags,
     sceneContainerFlags: SceneContainerFlags,
-    deviceEntryRepository: DeviceEntryRepository,
     bouncerRepository: KeyguardBouncerRepository,
     configurationRepository: ConfigurationRepository,
     shadeRepository: ShadeRepository,
@@ -160,7 +158,7 @@
     val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing
 
     /** Whether the keyguard is unlocked or not. */
-    val isKeyguardUnlocked: Flow<Boolean> = deviceEntryRepository.isUnlocked
+    val isKeyguardUnlocked: Flow<Boolean> = repository.isKeyguardUnlocked
 
     /** Whether the keyguard is occluded (covered by an activity). */
     val isKeyguardOccluded: Flow<Boolean> = repository.isKeyguardOccluded
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt
index 122c4c4..55b420b0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt
@@ -29,8 +29,10 @@
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
 import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 
 /** Handles key events arriving when the keyguard is showing or device is dozing. */
+@ExperimentalCoroutinesApi
 @SysUISingleton
 class KeyguardKeyEventInteractor
 @Inject
@@ -53,9 +55,14 @@
         }
 
         if (event.handleAction()) {
+            if (KeyEvent.isConfirmKey(event.keyCode)) {
+                if (isDeviceAwake()) {
+                    return collapseShadeLockedOrShowPrimaryBouncer()
+                }
+            }
+
             when (event.keyCode) {
                 KeyEvent.KEYCODE_MENU -> return dispatchMenuKeyEvent()
-                KeyEvent.KEYCODE_SPACE -> return dispatchSpaceEvent()
             }
         }
         return false
@@ -91,16 +98,22 @@
                 (statusBarStateController.state != StatusBarState.SHADE) &&
                 statusBarKeyguardViewManager.shouldDismissOnMenuPressed()
         if (shouldUnlockOnMenuPressed) {
-            shadeController.animateCollapseShadeForced()
-            return true
+            return collapseShadeLockedOrShowPrimaryBouncer()
         }
         return false
     }
 
-    private fun dispatchSpaceEvent(): Boolean {
-        if (isDeviceAwake() && statusBarStateController.state != StatusBarState.SHADE) {
-            shadeController.animateCollapseShadeForced()
-            return true
+    private fun collapseShadeLockedOrShowPrimaryBouncer(): Boolean {
+        when (statusBarStateController.state) {
+            StatusBarState.SHADE -> return false
+            StatusBarState.SHADE_LOCKED -> {
+                shadeController.animateCollapseShadeForced()
+                return true
+            }
+            StatusBarState.KEYGUARD -> {
+                statusBarKeyguardViewManager.showPrimaryBouncer(true)
+                return true
+            }
         }
         return false
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
new file mode 100644
index 0000000..e82ea7f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.ui.binder
+
+import android.annotation.SuppressLint
+import android.content.res.ColorStateList
+import android.view.View
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.common.ui.view.LongPressHandlingView
+import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
+import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.plugins.FalsingManager
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+
+@ExperimentalCoroutinesApi
+object DeviceEntryIconViewBinder {
+
+    /**
+     * Updates UI for the device entry icon view (lock, unlock and fingerprint icons) and its
+     * background.
+     */
+    @SuppressLint("ClickableViewAccessibility")
+    @JvmStatic
+    fun bind(
+        view: DeviceEntryIconView,
+        viewModel: DeviceEntryIconViewModel,
+        falsingManager: FalsingManager,
+    ) {
+        val iconView = view.iconView
+        val bgView = view.bgView
+        val longPressHandlingView = view.longPressHandlingView
+        longPressHandlingView.listener =
+            object : LongPressHandlingView.Listener {
+                override fun onLongPressDetected(view: View, x: Int, y: Int) {
+                    if (falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) {
+                        return
+                    }
+                    viewModel.onLongPress()
+                }
+            }
+        view.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                launch {
+                    viewModel.iconViewModel.collect { iconViewModel ->
+                        iconView.setImageState(
+                            view.getIconState(iconViewModel.type, iconViewModel.useAodVariant),
+                            /* merge */ false
+                        )
+                        iconView.imageTintList = ColorStateList.valueOf(iconViewModel.tint)
+                        iconView.alpha = iconViewModel.alpha
+                        iconView.setPadding(
+                            iconViewModel.padding,
+                            iconViewModel.padding,
+                            iconViewModel.padding,
+                            iconViewModel.padding,
+                        )
+                    }
+                }
+                launch {
+                    viewModel.backgroundViewModel.collect { bgViewModel ->
+                        bgView.alpha = bgViewModel.alpha
+                        bgView.imageTintList = ColorStateList.valueOf(bgViewModel.tint)
+                    }
+                }
+                launch {
+                    viewModel.burnInViewModel.collect { burnInViewModel ->
+                        view.translationX = burnInViewModel.x.toFloat()
+                        view.translationY = burnInViewModel.y.toFloat()
+                        view.aodFpDrawable.progress = burnInViewModel.progress
+                    }
+                }
+                launch {
+                    viewModel.isLongPressEnabled.collect { isEnabled ->
+                        longPressHandlingView.setLongPressHandlingEnabled(isEnabled)
+                    }
+                }
+                launch {
+                    viewModel.accessibilityDelegateHint.collect { hint ->
+                        view.accessibilityHintType = hint
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt
new file mode 100644
index 0000000..c9e3954
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt
@@ -0,0 +1,272 @@
+/*
+ * 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.ui.view
+
+import android.content.Context
+import android.graphics.drawable.AnimatedStateListDrawable
+import android.graphics.drawable.AnimatedVectorDrawable
+import android.util.AttributeSet
+import android.util.StateSet
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup
+import android.view.accessibility.AccessibilityNodeInfo
+import android.widget.FrameLayout
+import android.widget.ImageView
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
+import com.airbnb.lottie.LottieCompositionFactory
+import com.airbnb.lottie.LottieDrawable
+import com.android.systemui.common.ui.view.LongPressHandlingView
+import com.android.systemui.res.R
+
+class DeviceEntryIconView
+@JvmOverloads
+constructor(
+    context: Context,
+    attrs: AttributeSet?,
+    defStyleAttrs: Int = 0,
+) : FrameLayout(context, attrs, defStyleAttrs) {
+    val longPressHandlingView: LongPressHandlingView = LongPressHandlingView(context, attrs)
+    val iconView: ImageView = ImageView(context, attrs).apply { id = R.id.device_entry_icon_fg }
+    val bgView: ImageView = ImageView(context, attrs).apply { id = R.id.device_entry_icon_bg }
+    val aodFpDrawable: LottieDrawable = LottieDrawable()
+    var accessibilityHintType: AccessibilityHintType = AccessibilityHintType.NONE
+
+    private var animatedIconDrawable: AnimatedStateListDrawable = AnimatedStateListDrawable()
+
+    init {
+        setupIconStates()
+        setupIconTransitions()
+        setupAccessibilityDelegate()
+
+        // Ordering matters. From background to foreground we want:
+        //     bgView, iconView, longpressHandlingView overlay
+        addBgImageView()
+        addIconImageView()
+        addLongpressHandlingView()
+    }
+
+    private fun setupAccessibilityDelegate() {
+        accessibilityDelegate =
+            object : AccessibilityDelegate() {
+                private val accessibilityAuthenticateHint =
+                    AccessibilityNodeInfo.AccessibilityAction(
+                        AccessibilityNodeInfoCompat.ACTION_CLICK,
+                        resources.getString(R.string.accessibility_authenticate_hint)
+                    )
+                private val accessibilityEnterHint =
+                    AccessibilityNodeInfo.AccessibilityAction(
+                        AccessibilityNodeInfoCompat.ACTION_CLICK,
+                        resources.getString(R.string.accessibility_enter_hint)
+                    )
+                override fun onInitializeAccessibilityNodeInfo(
+                    v: View,
+                    info: AccessibilityNodeInfo
+                ) {
+                    super.onInitializeAccessibilityNodeInfo(v, info)
+                    when (accessibilityHintType) {
+                        AccessibilityHintType.AUTHENTICATE ->
+                            info.addAction(accessibilityAuthenticateHint)
+                        AccessibilityHintType.ENTER -> info.addAction(accessibilityEnterHint)
+                        AccessibilityHintType.NONE -> return
+                    }
+                }
+            }
+    }
+
+    private fun setupIconStates() {
+        // Lockscreen States
+        // LOCK
+        animatedIconDrawable.addState(
+            getIconState(IconType.LOCK, false),
+            context.getDrawable(R.drawable.ic_lock)!!,
+            R.id.locked,
+        )
+        // UNLOCK
+        animatedIconDrawable.addState(
+            getIconState(IconType.UNLOCK, false),
+            context.getDrawable(R.drawable.ic_unlocked)!!,
+            R.id.unlocked,
+        )
+        // FINGERPRINT
+        animatedIconDrawable.addState(
+            getIconState(IconType.FINGERPRINT, false),
+            context.getDrawable(R.drawable.ic_kg_fingerprint)!!,
+            R.id.locked_fp,
+        )
+
+        // AOD states
+        // LOCK
+        animatedIconDrawable.addState(
+            getIconState(IconType.LOCK, true),
+            context.getDrawable(R.drawable.ic_lock_aod)!!,
+            R.id.locked_aod,
+        )
+        // UNLOCK
+        animatedIconDrawable.addState(
+            getIconState(IconType.UNLOCK, true),
+            context.getDrawable(R.drawable.ic_unlocked_aod)!!,
+            R.id.unlocked_aod,
+        )
+        // FINGERPRINT
+        LottieCompositionFactory.fromRawRes(mContext, R.raw.udfps_aod_fp).addListener { result ->
+            aodFpDrawable.setComposition(result)
+        }
+        animatedIconDrawable.addState(
+            getIconState(IconType.FINGERPRINT, true),
+            aodFpDrawable,
+            R.id.udfps_aod_fp,
+        )
+
+        // WILDCARD: should always be the last state added since any states will match with this
+        // and therefore won't get matched with subsequent states.
+        animatedIconDrawable.addState(
+            StateSet.WILD_CARD,
+            context.getDrawable(R.color.transparent)!!,
+            R.id.no_icon,
+        )
+    }
+
+    private fun setupIconTransitions() {
+        // LockscreenFp <=> LockscreenUnlocked
+        animatedIconDrawable.addTransition(
+            R.id.locked_fp,
+            R.id.unlocked,
+            context.getDrawable(R.drawable.fp_to_unlock) as AnimatedVectorDrawable,
+            /* reversible */ false,
+        )
+        animatedIconDrawable.addTransition(
+            R.id.unlocked,
+            R.id.locked_fp,
+            context.getDrawable(R.drawable.unlock_to_fp) as AnimatedVectorDrawable,
+            /* reversible */ false,
+        )
+
+        // LockscreenLocked <=> AodLocked
+        animatedIconDrawable.addTransition(
+            R.id.locked_aod,
+            R.id.locked,
+            context.getDrawable(R.drawable.lock_aod_to_ls) as AnimatedVectorDrawable,
+            /* reversible */ false,
+        )
+        animatedIconDrawable.addTransition(
+            R.id.locked,
+            R.id.locked_aod,
+            context.getDrawable(R.drawable.lock_ls_to_aod) as AnimatedVectorDrawable,
+            /* reversible */ false,
+        )
+
+        // LockscreenUnlocked <=> AodUnlocked
+        animatedIconDrawable.addTransition(
+            R.id.unlocked_aod,
+            R.id.unlocked,
+            context.getDrawable(R.drawable.unlocked_aod_to_ls) as AnimatedVectorDrawable,
+            /* reversible */ false,
+        )
+        animatedIconDrawable.addTransition(
+            R.id.unlocked,
+            R.id.unlocked_aod,
+            context.getDrawable(R.drawable.unlocked_ls_to_aod) as AnimatedVectorDrawable,
+            /* reversible */ false,
+        )
+
+        // LockscreenLocked <=> LockscreenUnlocked
+        animatedIconDrawable.addTransition(
+            R.id.locked,
+            R.id.unlocked,
+            context.getDrawable(R.drawable.lock_to_unlock) as AnimatedVectorDrawable,
+            /* reversible */ false,
+        )
+        animatedIconDrawable.addTransition(
+            R.id.unlocked,
+            R.id.locked,
+            context.getDrawable(R.drawable.unlocked_to_locked) as AnimatedVectorDrawable,
+            /* reversible */ false,
+        )
+
+        // LockscreenFingerprint <=> LockscreenLocked
+        animatedIconDrawable.addTransition(
+            R.id.locked_fp,
+            R.id.locked,
+            context.getDrawable(R.drawable.fp_to_locked) as AnimatedVectorDrawable,
+            /* reversible */ true,
+        )
+
+        // LockscreenUnlocked <=> AodLocked
+        animatedIconDrawable.addTransition(
+            R.id.unlocked,
+            R.id.locked_aod,
+            context.getDrawable(R.drawable.unlocked_to_aod_lock) as AnimatedVectorDrawable,
+            /* reversible */ true,
+        )
+    }
+
+    private fun addLongpressHandlingView() {
+        addView(longPressHandlingView)
+        val lp = longPressHandlingView.layoutParams as LayoutParams
+        lp.height = ViewGroup.LayoutParams.MATCH_PARENT
+        lp.width = ViewGroup.LayoutParams.MATCH_PARENT
+        longPressHandlingView.setLayoutParams(lp)
+    }
+
+    private fun addIconImageView() {
+        iconView.scaleType = ImageView.ScaleType.CENTER_CROP
+        iconView.setImageDrawable(animatedIconDrawable)
+        addView(iconView)
+        val lp = iconView.layoutParams as LayoutParams
+        lp.height = ViewGroup.LayoutParams.MATCH_PARENT
+        lp.width = ViewGroup.LayoutParams.MATCH_PARENT
+        lp.gravity = Gravity.CENTER
+        iconView.setLayoutParams(lp)
+    }
+
+    private fun addBgImageView() {
+        bgView.setImageDrawable(context.getDrawable(R.drawable.fingerprint_bg))
+        addView(bgView)
+        val lp = bgView.layoutParams as LayoutParams
+        lp.height = ViewGroup.LayoutParams.MATCH_PARENT
+        lp.width = ViewGroup.LayoutParams.MATCH_PARENT
+        bgView.setLayoutParams(lp)
+    }
+
+    fun getIconState(icon: IconType, aod: Boolean): IntArray {
+        val lockIconState = IntArray(2)
+        when (icon) {
+            IconType.LOCK -> lockIconState[0] = android.R.attr.state_first
+            IconType.UNLOCK -> lockIconState[0] = android.R.attr.state_last
+            IconType.FINGERPRINT -> lockIconState[0] = android.R.attr.state_middle
+        }
+        if (aod) {
+            lockIconState[1] = android.R.attr.state_single
+        } else {
+            lockIconState[1] = -android.R.attr.state_single
+        }
+        return lockIconState
+    }
+
+    enum class IconType {
+        LOCK,
+        UNLOCK,
+        FINGERPRINT,
+    }
+
+    enum class AccessibilityHintType {
+        NONE,
+        AUTHENTICATE,
+        ENTER,
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt
index d8e4396..21eba56 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt
@@ -23,8 +23,8 @@
 import com.android.systemui.keyguard.ui.view.layout.sections.AodBurnInSection
 import com.android.systemui.keyguard.ui.view.layout.sections.AodNotificationIconsSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultAmbientIndicationAreaSection
+import com.android.systemui.keyguard.ui.view.layout.sections.DefaultDeviceEntryIconSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultIndicationAreaSection
-import com.android.systemui.keyguard.ui.view.layout.sections.DefaultLockIconSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultNotificationStackScrollLayoutSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultSettingsPopupMenuSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultShortcutsSection
@@ -44,7 +44,7 @@
 @Inject
 constructor(
     defaultIndicationAreaSection: DefaultIndicationAreaSection,
-    defaultLockIconSection: DefaultLockIconSection,
+    defaultDeviceEntryIconSection: DefaultDeviceEntryIconSection,
     defaultShortcutsSection: DefaultShortcutsSection,
     defaultAmbientIndicationAreaSection: DefaultAmbientIndicationAreaSection,
     defaultSettingsPopupMenuSection: DefaultSettingsPopupMenuSection,
@@ -61,7 +61,7 @@
     override val sections =
         setOf(
             defaultIndicationAreaSection,
-            defaultLockIconSection,
+            defaultDeviceEntryIconSection,
             defaultShortcutsSection,
             defaultAmbientIndicationAreaSection,
             defaultSettingsPopupMenuSection,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt
index ce76f56..f8dd7c1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt
@@ -23,8 +23,8 @@
 import com.android.systemui.keyguard.ui.view.layout.sections.AodBurnInSection
 import com.android.systemui.keyguard.ui.view.layout.sections.AodNotificationIconsSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultAmbientIndicationAreaSection
+import com.android.systemui.keyguard.ui.view.layout.sections.DefaultDeviceEntryIconSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultIndicationAreaSection
-import com.android.systemui.keyguard.ui.view.layout.sections.DefaultLockIconSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultNotificationStackScrollLayoutSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultSettingsPopupMenuSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection
@@ -38,7 +38,7 @@
 @Inject
 constructor(
     defaultIndicationAreaSection: DefaultIndicationAreaSection,
-    defaultLockIconSection: DefaultLockIconSection,
+    defaultDeviceEntryIconSection: DefaultDeviceEntryIconSection,
     defaultAmbientIndicationAreaSection: DefaultAmbientIndicationAreaSection,
     defaultSettingsPopupMenuSection: DefaultSettingsPopupMenuSection,
     alignShortcutsToUdfpsSection: AlignShortcutsToUdfpsSection,
@@ -54,7 +54,7 @@
     override val sections =
         setOf(
             defaultIndicationAreaSection,
-            defaultLockIconSection,
+            defaultDeviceEntryIconSection,
             defaultAmbientIndicationAreaSection,
             defaultSettingsPopupMenuSection,
             alignShortcutsToUdfpsSection,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
index 669db34..b7fe960 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
@@ -26,23 +26,40 @@
 import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
 import androidx.constraintlayout.widget.ConstraintSet.START
 import androidx.constraintlayout.widget.ConstraintSet.TOP
-import com.android.systemui.res.R
-import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.common.ui.ConfigurationState
+import com.android.systemui.flags.FeatureFlagsClassic
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.shared.model.KeyguardSection
+import com.android.systemui.res.R
 import com.android.systemui.shade.NotificationPanelView
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel
+import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
+import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.phone.NotificationIconAreaController
 import com.android.systemui.statusbar.phone.NotificationIconContainer
+import com.android.systemui.statusbar.phone.ScreenOffAnimationController
+import com.android.systemui.statusbar.policy.ConfigurationController
 import javax.inject.Inject
+import kotlinx.coroutines.DisposableHandle
 
 class AodNotificationIconsSection
 @Inject
 constructor(
     private val context: Context,
-    private val featureFlags: FeatureFlags,
+    private val configurationState: ConfigurationState,
+    private val configurationController: ConfigurationController,
+    private val dozeParameters: DozeParameters,
+    private val featureFlags: FeatureFlagsClassic,
+    private val nicAodViewModel: NotificationIconContainerAlwaysOnDisplayViewModel,
+    private val nicAodIconViewStore: AlwaysOnDisplayNotificationIconViewStore,
     private val notificationPanelView: NotificationPanelView,
     private val notificationIconAreaController: NotificationIconAreaController,
+    private val screenOffAnimationController: ScreenOffAnimationController,
 ) : KeyguardSection() {
+
+    private var nicBindingDisposable: DisposableHandle? = null
     private val nicId = R.id.aod_notification_icon_container
     private lateinit var nic: NotificationIconContainer
 
@@ -70,7 +87,23 @@
             return
         }
 
-        notificationIconAreaController.setupAodIcons(nic)
+        if (NotificationIconContainerRefactor.isEnabled) {
+            nic.setOnLockScreen(true)
+            nicBindingDisposable?.dispose()
+            nicBindingDisposable =
+                NotificationIconContainerViewBinder.bind(
+                    nic,
+                    nicAodViewModel,
+                    configurationState,
+                    configurationController,
+                    dozeParameters,
+                    featureFlags,
+                    screenOffAnimationController,
+                    nicAodIconViewStore,
+                )
+        } else {
+            notificationIconAreaController.setupAodIcons(nic)
+        }
     }
 
     override fun applyConstraints(constraintSet: ConstraintSet) {
@@ -102,5 +135,6 @@
 
     override fun removeViews(constraintLayout: ConstraintLayout) {
         constraintLayout.removeView(nicId)
+        nicBindingDisposable?.dispose()
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultLockIconSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSection.kt
similarity index 63%
rename from packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultLockIconSection.kt
rename to packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSection.kt
index f4bc713..62c5988 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultLockIconSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSection.kt
@@ -33,11 +33,18 @@
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.shared.model.KeyguardSection
+import com.android.systemui.keyguard.ui.binder.DeviceEntryIconViewBinder
+import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
+import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel
+import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.res.R
 import com.android.systemui.shade.NotificationPanelView
+import dagger.Lazy
 import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 
-class DefaultLockIconSection
+@ExperimentalCoroutinesApi
+class DefaultDeviceEntryIconSection
 @Inject
 constructor(
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
@@ -46,24 +53,47 @@
     private val context: Context,
     private val notificationPanelView: NotificationPanelView,
     private val featureFlags: FeatureFlags,
-    private val lockIconViewController: LockIconViewController,
+    private val lockIconViewController: Lazy<LockIconViewController>,
+    private val deviceEntryIconViewModel: Lazy<DeviceEntryIconViewModel>,
+    private val falsingManager: Lazy<FalsingManager>,
 ) : KeyguardSection() {
-    private val lockIconViewId = R.id.lock_icon_view
+    private val deviceEntryIconViewId = R.id.device_entry_icon_view
 
     override fun addViews(constraintLayout: ConstraintLayout) {
-        if (!featureFlags.isEnabled(Flags.MIGRATE_LOCK_ICON)) {
+        if (
+            !featureFlags.isEnabled(Flags.MIGRATE_LOCK_ICON) &&
+                !featureFlags.isEnabled(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS)
+        ) {
             return
         }
-        notificationPanelView.findViewById<View>(lockIconViewId).let {
+
+        notificationPanelView.findViewById<View>(R.id.lock_icon_view).let {
             notificationPanelView.removeView(it)
         }
-        val view = LockIconView(context, null).apply { id = lockIconViewId }
+
+        val view =
+            if (featureFlags.isEnabled(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS)) {
+                DeviceEntryIconView(context, null).apply { id = deviceEntryIconViewId }
+            } else {
+                // Flags.MIGRATE_LOCK_ICON
+                LockIconView(context, null).apply { id = deviceEntryIconViewId }
+            }
         constraintLayout.addView(view)
     }
 
     override fun bindData(constraintLayout: ConstraintLayout) {
-        constraintLayout.findViewById<LockIconView?>(lockIconViewId)?.let {
-            lockIconViewController.setLockIconView(it)
+        if (featureFlags.isEnabled(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS)) {
+            constraintLayout.findViewById<DeviceEntryIconView?>(deviceEntryIconViewId)?.let {
+                DeviceEntryIconViewBinder.bind(
+                    it,
+                    deviceEntryIconViewModel.get(),
+                    falsingManager.get(),
+                )
+            }
+        } else {
+            constraintLayout.findViewById<LockIconView?>(deviceEntryIconViewId)?.let {
+                lockIconViewController.get().setLockIconView(it)
+            }
         }
     }
 
@@ -84,30 +114,30 @@
         val defaultDensity =
             DisplayMetrics.DENSITY_DEVICE_STABLE.toFloat() /
                 DisplayMetrics.DENSITY_DEFAULT.toFloat()
-        val lockIconRadiusPx = (defaultDensity * 36).toInt()
+        val iconRadiusPx = (defaultDensity * 36).toInt()
 
         if (isUdfpsSupported) {
             authController.udfpsLocation?.let { udfpsLocation ->
-                centerLockIcon(udfpsLocation, authController.udfpsRadius, constraintSet)
+                centerIcon(udfpsLocation, authController.udfpsRadius, constraintSet)
             }
         } else {
-            centerLockIcon(
+            centerIcon(
                 Point(
                     (widthPixels / 2).toInt(),
-                    (heightPixels - ((mBottomPaddingPx + lockIconRadiusPx) * scaleFactor)).toInt()
+                    (heightPixels - ((mBottomPaddingPx + iconRadiusPx) * scaleFactor)).toInt()
                 ),
-                lockIconRadiusPx * scaleFactor,
+                iconRadiusPx * scaleFactor,
                 constraintSet,
             )
         }
     }
 
     override fun removeViews(constraintLayout: ConstraintLayout) {
-        constraintLayout.removeView(lockIconViewId)
+        constraintLayout.removeView(deviceEntryIconViewId)
     }
 
     @VisibleForTesting
-    internal fun centerLockIcon(center: Point, radius: Float, constraintSet: ConstraintSet) {
+    internal fun centerIcon(center: Point, radius: Float, constraintSet: ConstraintSet) {
         val sensorRect =
             Rect().apply {
                 set(
@@ -119,17 +149,17 @@
             }
 
         constraintSet.apply {
-            constrainWidth(lockIconViewId, sensorRect.right - sensorRect.left)
-            constrainHeight(lockIconViewId, sensorRect.bottom - sensorRect.top)
+            constrainWidth(deviceEntryIconViewId, sensorRect.right - sensorRect.left)
+            constrainHeight(deviceEntryIconViewId, sensorRect.bottom - sensorRect.top)
             connect(
-                lockIconViewId,
+                deviceEntryIconViewId,
                 ConstraintSet.TOP,
                 ConstraintSet.PARENT_ID,
                 ConstraintSet.TOP,
                 sensorRect.top
             )
             connect(
-                lockIconViewId,
+                deviceEntryIconViewId,
                 ConstraintSet.START,
                 ConstraintSet.PARENT_ID,
                 ConstraintSet.START,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
new file mode 100644
index 0000000..842dde3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.ui.viewmodel
+
+import android.graphics.Color
+import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+
+@ExperimentalCoroutinesApi
+class DeviceEntryIconViewModel @Inject constructor() {
+    // TODO: b/305234447 update these states from the data layer
+    val iconViewModel: Flow<IconViewModel> =
+        flowOf(
+            IconViewModel(
+                type = DeviceEntryIconView.IconType.LOCK,
+                useAodVariant = false,
+                tint = Color.WHITE,
+                alpha = 1f,
+                padding = 48,
+            )
+        )
+    val backgroundViewModel: Flow<BackgroundViewModel> =
+        flowOf(BackgroundViewModel(alpha = 1f, tint = Color.GRAY))
+    val burnInViewModel: Flow<BurnInViewModel> = flowOf(BurnInViewModel(0, 0, 0f))
+    val isLongPressEnabled: Flow<Boolean> = flowOf(true)
+    val accessibilityDelegateHint: Flow<DeviceEntryIconView.AccessibilityHintType> =
+        flowOf(DeviceEntryIconView.AccessibilityHintType.NONE)
+
+    fun onLongPress() {
+        // TODO() vibrate & perform action based on current lock/unlock state
+    }
+    data class BurnInViewModel(
+        val x: Int, // current x burn in offset based on the aodTransitionAmount
+        val y: Int, // current y burn in offset based on the aodTransitionAmount
+        val progress: Float, // current progress based on the aodTransitionAmount
+    )
+
+    class IconViewModel(
+        val type: DeviceEntryIconView.IconType,
+        val useAodVariant: Boolean,
+        val tint: Int,
+        val alpha: Float,
+        val padding: Int,
+    )
+
+    class BackgroundViewModel(
+        val alpha: Float,
+        val tint: Int,
+    )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileLifecycle.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/BluetoothTileDialogLog.kt
similarity index 69%
copy from packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileLifecycle.kt
copy to packages/SystemUI/src/com/android/systemui/log/dagger/BluetoothTileDialogLog.kt
index 6d7c576..c73afe8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileLifecycle.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/BluetoothTileDialogLog.kt
@@ -14,9 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.systemui.qs.tiles.viewmodel
+package com.android.systemui.log.dagger
 
-enum class QSTileLifecycle {
-    ALIVE,
-    DEAD,
-}
+import javax.inject.Qualifier
+
+/** A [com.android.systemui.log.LogBuffer] for Bluetooth QS tile dialog related logging. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class BluetoothTileDialogLog
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 fd6b3f1..e768f16 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -91,7 +91,7 @@
     @SysUISingleton
     @NotifInflationLog
     public static LogBuffer provideNotifInflationLogBuffer(LogBufferFactory factory) {
-        return factory.create("NotifInflationLog", 100);
+        return factory.create("NotifInflationLog", 250);
     }
 
     /** Provides a logging buffer for notification interruption calculations. */
@@ -552,4 +552,12 @@
     public static LogBuffer provideSceneFrameworkLogBuffer(LogBufferFactory factory) {
         return factory.create("SceneFramework", 50);
     }
+
+    /** Provides a {@link LogBuffer} for the bluetooth QS tile dialog. */
+    @Provides
+    @SysUISingleton
+    @BluetoothTileDialogLog
+    public static LogBuffer provideQBluetoothTileDialogLogBuffer(LogBufferFactory factory) {
+        return factory.create("BluetoothTileDialogLog", 50);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt
index 1db31ae..2034d97 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt
@@ -47,6 +47,7 @@
 import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManagerFactory
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.policy.ConfigurationController
+import dagger.Lazy
 import java.io.PrintWriter
 import java.util.concurrent.Executor
 import javax.inject.Inject
@@ -62,10 +63,10 @@
     private val context: Context,
     private val controllerFactory: MediaControllerFactory,
     private val localMediaManagerFactory: LocalMediaManagerFactory,
-    private val mr2manager: MediaRouter2Manager,
+    private val mr2manager: Lazy<MediaRouter2Manager>,
     private val muteAwaitConnectionManagerFactory: MediaMuteAwaitConnectionManagerFactory,
     private val configurationController: ConfigurationController,
-    private val localBluetoothManager: LocalBluetoothManager?,
+    private val localBluetoothManager: Lazy<LocalBluetoothManager?>,
     @Main private val fgExecutor: Executor,
     @Background private val bgExecutor: Executor,
     dumpManager: DumpManager,
@@ -210,8 +211,8 @@
 
         fun dump(pw: PrintWriter) {
             val routingSession =
-                controller?.let { mr2manager.getRoutingSessionForMediaController(it) }
-            val selectedRoutes = routingSession?.let { mr2manager.getSelectedRoutes(it) }
+                controller?.let { mr2manager.get().getRoutingSessionForMediaController(it) }
+            val selectedRoutes = routingSession?.let { mr2manager.get().getSelectedRoutes(it) }
             with(pw) {
                 println("    current device is ${current?.name}")
                 val type = controller?.playbackInfo?.playbackType
@@ -355,7 +356,7 @@
                 val device =
                     aboutToConnect?.fullMediaDevice ?: localMediaManager.currentConnectedDevice
                 val routingSession =
-                    controller?.let { mr2manager.getRoutingSessionForMediaController(it) }
+                    controller?.let { mr2manager.get().getRoutingSessionForMediaController(it) }
 
                 // If we have a controller but get a null route, then don't trust the device
                 val enabled = device != null && (controller == null || routingSession != null)
@@ -380,7 +381,7 @@
             device: MediaDevice?,
             routingSession: RoutingSessionInfo?,
         ): String? {
-            val selectedRoutes = routingSession?.let { mr2manager.getSelectedRoutes(it) }
+            val selectedRoutes = routingSession?.let { mr2manager.get().getSelectedRoutes(it) }
 
             if (DEBUG) {
                 Log.d(
@@ -426,7 +427,9 @@
             return null
         }
 
+        @WorkerThread
         private fun isLeAudioBroadcastEnabled(): Boolean {
+            val localBluetoothManager = localBluetoothManager.get()
             if (localBluetoothManager != null) {
                 val profileManager = localBluetoothManager.profileManager
                 if (profileManager != null) {
@@ -446,19 +449,20 @@
             return false
         }
 
+        @WorkerThread
         private fun getBroadcastingInfo(bluetoothLeBroadcast: LocalBluetoothLeBroadcast) {
-            var currentBroadcastedApp = bluetoothLeBroadcast.appSourceName
+            val currentBroadcastedApp = bluetoothLeBroadcast.appSourceName
             // TODO(b/233698402): Use the package name instead of app label to avoid the
             // unexpected result.
             // Check the current media app's name is the same with current broadcast app's name
             // or not.
-            var mediaApp =
+            val mediaApp =
                 MediaDataUtils.getAppLabel(
                     context,
                     localMediaManager.packageName,
                     context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name)
                 )
-            var isCurrentBroadcastedApp = TextUtils.equals(mediaApp, currentBroadcastedApp)
+            val isCurrentBroadcastedApp = TextUtils.equals(mediaApp, currentBroadcastedApp)
             if (isCurrentBroadcastedApp) {
                 broadcastDescription =
                     context.getString(R.string.broadcasting_description_is_broadcasting)
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionMetricsLogger.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionMetricsLogger.kt
index 1962119..98a3896 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionMetricsLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionMetricsLogger.kt
@@ -16,7 +16,6 @@
 package com.android.systemui.mediaprojection
 
 import android.media.projection.IMediaProjectionManager
-import android.os.Process
 import android.os.RemoteException
 import android.util.Log
 import com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__CREATION_SOURCE__CREATION_SOURCE_APP as METRICS_CREATION_SOURCE_APP
@@ -66,6 +65,19 @@
     }
 
     /**
+     * Request to log that the permission request was cancelled.
+     *
+     * @param hostUid The UID of the package that initiates MediaProjection.
+     */
+    fun notifyProjectionRequestCancelled(hostUid: Int) {
+        try {
+            service.notifyPermissionRequestCancelled(hostUid)
+        } catch (e: RemoteException) {
+            Log.e(TAG, "Error notifying server of projection cancelled", e)
+        }
+    }
+
+    /**
      * Request to log that the app selector was displayed.
      *
      * @param hostUid The UID of the package that initiates MediaProjection.
@@ -78,47 +90,6 @@
         }
     }
 
-    /**
-     * Request to log that the permission request moved to the given state.
-     *
-     * Should not be used for the initialization state, since that should use {@link
-     * MediaProjectionMetricsLogger#notifyProjectionInitiated(Int)} and pass the
-     * sessionCreationSource.
-     */
-    fun notifyPermissionProgress(state: Int) {
-        // TODO validate state is valid
-        notifyToServer(state, SessionCreationSource.UNKNOWN)
-    }
-
-    /**
-     * Notifies system server that we are handling a particular state during the consent flow.
-     *
-     * Only used for emitting atoms.
-     *
-     * @param state The state that SystemUI is handling during the consent flow. Must be a valid
-     *   state defined in the MediaProjectionState enum.
-     * @param sessionCreationSource Only set if the state is MEDIA_PROJECTION_STATE_INITIATED.
-     *   Indicates the entry point for requesting the permission. Must be a valid state defined in
-     *   the SessionCreationSource enum.
-     */
-    private fun notifyToServer(state: Int, sessionCreationSource: SessionCreationSource) {
-        Log.v(TAG, "FOO notifyToServer of state $state and source $sessionCreationSource")
-        try {
-            service.notifyPermissionRequestStateChange(
-                Process.myUid(),
-                state,
-                sessionCreationSource.toMetricsConstant()
-            )
-        } catch (e: RemoteException) {
-            Log.e(
-                TAG,
-                "Error notifying server of permission flow state $state from source " +
-                    "$sessionCreationSource",
-                e
-            )
-        }
-    }
-
     companion object {
         const val TAG = "MediaProjectionMetricsLogger"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt
index 04d5566..50e9e751 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt
@@ -210,6 +210,10 @@
                 reviewGrantedConsentRequired,
                 /* projection= */ null
             )
+            if (isFinishing) {
+                // Only log dismissed when actually finishing, and not when changing configuration.
+                controller.onSelectorDismissed()
+            }
         }
         activityLauncher.destroy()
         controller.destroy()
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt
index 67ef119..575e198 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt
@@ -18,7 +18,6 @@
 
 import android.content.ComponentName
 import android.os.UserHandle
-import com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_APP_SELECTOR_DISPLAYED as STATE_APP_SELECTOR_DISPLAYED
 import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
 import com.android.systemui.mediaprojection.appselector.data.RecentTask
 import com.android.systemui.mediaprojection.appselector.data.RecentTaskListProvider
@@ -76,6 +75,10 @@
         scope.cancel()
     }
 
+    fun onSelectorDismissed() {
+        logger.notifyProjectionRequestCancelled(hostUid)
+    }
+
     private suspend fun refreshForegroundTaskThumbnails(tasks: List<RecentTask>) {
         coroutineScope {
             val thumbnails: List<Deferred<ThumbnailData?>> =
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseScreenSharePermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseScreenSharePermissionDialog.kt
index 8b437c3..eea369f 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseScreenSharePermissionDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseScreenSharePermissionDialog.kt
@@ -32,6 +32,7 @@
 import androidx.annotation.DrawableRes
 import androidx.annotation.LayoutRes
 import androidx.annotation.StringRes
+import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.phone.SystemUIDialog
 
@@ -40,16 +41,31 @@
     context: Context,
     private val screenShareOptions: List<ScreenShareOption>,
     private val appName: String?,
+    private val hostUid: Int,
+    private val mediaProjectionMetricsLogger: MediaProjectionMetricsLogger,
     @DrawableRes private val dialogIconDrawable: Int? = null,
-    @ColorRes private val dialogIconTint: Int? = null
+    @ColorRes private val dialogIconTint: Int? = null,
 ) : SystemUIDialog(context), AdapterView.OnItemSelectedListener {
     private lateinit var dialogTitle: TextView
     private lateinit var startButton: TextView
     private lateinit var cancelButton: TextView
     private lateinit var warning: TextView
     private lateinit var screenShareModeSpinner: Spinner
+    private var hasCancelBeenLogged: Boolean = false
     var selectedScreenShareOption: ScreenShareOption = screenShareOptions.first()
 
+    override fun dismiss() {
+        super.dismiss()
+
+        // Dismiss can be called multiple times and we only want to log once.
+        if (hasCancelBeenLogged) {
+            return
+        }
+
+        mediaProjectionMetricsLogger.notifyProjectionRequestCancelled(hostUid)
+        hasCancelBeenLogged = true
+    }
+
     public override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         window?.addPrivateFlags(WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS)
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
index f7cc589..eacfa57 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
@@ -222,13 +222,19 @@
         // the correct screen width when in split screen.
         Context dialogContext = getApplicationContext();
         if (isPartialScreenSharingEnabled()) {
-            mDialog = new MediaProjectionPermissionDialog(dialogContext, getMediaProjectionConfig(),
+            mDialog = new MediaProjectionPermissionDialog(
+                    dialogContext,
+                    getMediaProjectionConfig(),
                     () -> {
                         MediaProjectionPermissionDialog dialog =
                                 (MediaProjectionPermissionDialog) mDialog;
                         ScreenShareOption selectedOption = dialog.getSelectedScreenShareOption();
                         grantMediaProjectionPermission(selectedOption.getMode());
-                    }, () -> finish(RECORD_CANCEL, /* projection= */ null), appName);
+                    },
+                    () -> finish(RECORD_CANCEL, /* projection= */ null),
+                    appName,
+                    mUid,
+                    mMediaProjectionMetricsLogger);
         } else {
             AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(dialogContext,
                     R.style.Theme_SystemUI_Dialog)
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialog.kt
index b9bafd4..cff22b0 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialog.kt
@@ -18,6 +18,7 @@
 import android.content.Context
 import android.media.projection.MediaProjectionConfig
 import android.os.Bundle
+import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
 import com.android.systemui.res.R
 
 /** Dialog to select screen recording options */
@@ -26,12 +27,16 @@
     mediaProjectionConfig: MediaProjectionConfig?,
     private val onStartRecordingClicked: Runnable,
     private val onCancelClicked: Runnable,
-    private val appName: String?
+    private val appName: String?,
+    hostUid: Int,
+    mediaProjectionMetricsLogger: MediaProjectionMetricsLogger,
 ) :
     BaseScreenSharePermissionDialog(
         context,
         createOptionList(context, appName, mediaProjectionConfig),
-        appName
+        appName,
+        hostUid,
+        mediaProjectionMetricsLogger
     ) {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
index 38204ab..a6c6233 100644
--- a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
+++ b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
@@ -64,14 +64,13 @@
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.settingslib.Utils;
 import com.android.settingslib.fuelgauge.BatterySaverUtils;
-import com.android.settingslib.utils.PowerUtil;
-import com.android.systemui.res.R;
 import com.android.systemui.SystemUIApplication;
 import com.android.systemui.animation.DialogCuj;
 import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.broadcast.BroadcastSender;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.res.R;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 import com.android.systemui.statusbar.policy.BatteryController;
@@ -376,14 +375,6 @@
                 TAG_AUTO_SAVER, SystemMessage.NOTE_AUTO_SAVER_SUGGESTION, n, UserHandle.ALL);
     }
 
-    private String getHybridContentString(String percentage) {
-        return PowerUtil.getBatteryRemainingStringFormatted(
-                mContext,
-                mCurrentBatterySnapshot.getTimeRemainingMillis(),
-                percentage,
-                mCurrentBatterySnapshot.isBasedOnUsage());
-    }
-
     private PendingIntent pendingBroadcast(String action) {
         return PendingIntent.getBroadcastAsUser(
                 mContext,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentLegacy.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentLegacy.java
index 68bf88b..3b3844a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentLegacy.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentLegacy.java
@@ -74,6 +74,7 @@
     public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
         QSFragmentComponent qsFragmentComponent = mQsComponentFactory.create(getView());
         mQsImpl = mQsImplProvider.get();
+        mQsImpl.onCreate(null);
         mQsImpl.onComponentCreated(qsFragmentComponent, savedInstanceState);
     }
 
@@ -85,21 +86,13 @@
     }
 
     @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        if (mQsImpl != null) {
-            mQsImpl.onCreate(savedInstanceState);
-        }
-    }
-
-    @Override
-    public void onDestroy() {
-        super.onDestroy();
+    public void onDestroyView() {
         if (mQsImpl != null) {
             mQsImpl.onDestroy();
+            mQsImpl = null;
         }
+        super.onDestroyView();
     }
-
     @Override
     public void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
index 4aad6a0..fab7e95 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
@@ -170,6 +170,7 @@
     private CommandQueue mCommandQueue;
 
     private View mRootView;
+    private View mFooterActionsView;
 
     @Inject
     public QSImpl(RemoteInputQuickSettingsDisabler remoteInputQsDisabler,
@@ -285,6 +286,7 @@
         if (!mFeatureFlags.isEnabled(Flags.COMPOSE_QS_FOOTER_ACTIONS)
                 || !ComposeFacade.INSTANCE.isComposeAvailable()) {
             Log.d(TAG, "Binding the View implementation of the QS footer actions");
+            mFooterActionsView = footerActionsView;
             mFooterActionsViewBinder.bind(footerActionsView, mQSFooterActionsViewModel,
                     mListeningAndVisibilityLifecycleOwner);
             return;
@@ -294,6 +296,7 @@
         Log.d(TAG, "Binding the Compose implementation of the QS footer actions");
         View composeView = ComposeFacade.INSTANCE.createFooterActionsView(root.getContext(),
                 mQSFooterActionsViewModel, mListeningAndVisibilityLifecycleOwner);
+        mFooterActionsView = composeView;
 
         // The id R.id.qs_footer_actions is used by QSContainerImpl to set the horizontal margin
         // to all views except for qs_footer_actions, so we set it to the Compose view.
@@ -472,7 +475,7 @@
         boolean footerVisible = qsPanelVisible && (mQsExpanded || !keyguardShowing
                 || mHeaderAnimating || mShowCollapsedOnKeyguard);
         mFooter.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE);
-        mQSFooterActionsViewModel.onVisibilityChangeRequested(footerVisible);
+        mFooterActionsView.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE);
         mFooter.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard)
                 || (mQsExpanded && !mStackScrollerOverscrolling));
         mQSPanelController.setVisibility(qsPanelVisible ? View.VISIBLE : View.INVISIBLE);
@@ -856,7 +859,7 @@
         boolean customizing = isCustomizing();
         mQSPanelScrollView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
         mFooter.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
-        mQSFooterActionsViewModel.onVisibilityChangeRequested(!customizing);
+        mFooterActionsView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
         mHeader.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
         // Let the panel know the position changed and it needs to update where notifications
         // and whatnot are.
@@ -940,7 +943,7 @@
     @Override
     public void dump(PrintWriter pw, String[] args) {
         IndentingPrintWriter indentingPw = new IndentingPrintWriter(pw, /* singleIndent= */ "  ");
-        indentingPw.println("QSFragment:");
+        indentingPw.println("QSImpl:");
         indentingPw.increaseIndent();
         indentingPw.println("mQsBounds: " + mQsBounds);
         indentingPw.println("mQsExpanded: " + mQsExpanded);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index fc2f5f9..11db69b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -174,7 +174,9 @@
 
     @Override
     public void destroy() {
-        super.destroy();
+        // Don't call super as this may be called before the view is dettached and calling super
+        // will remove the attach listener. We don't need to do that, because once this object is
+        // detached from the graph, it will be gc.
         mHost.removeCallback(mQSHostCallback);
 
         for (TileRecord record : mRecords) {
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 a65967a..bd4c6e1 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
@@ -31,7 +31,7 @@
 import com.android.systemui.qs.external.QSExternalModule;
 import com.android.systemui.qs.pipeline.dagger.QSPipelineModule;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
-import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel;
+import com.android.systemui.qs.tiles.di.QSTilesModule;
 import com.android.systemui.statusbar.phone.AutoTileManager;
 import com.android.systemui.statusbar.phone.ManagedProfileController;
 import com.android.systemui.statusbar.policy.CastController;
@@ -60,20 +60,18 @@
                 QSFlagsModule.class,
                 QSHostModule.class,
                 QSPipelineModule.class,
+                QSTilesModule.class,
         }
 )
 public interface QSModule {
 
-    /** A map of internal QS tiles. Ensures that this can be injected even if
-     * it is empty */
+    /**
+     * A map of internal QS tiles. Ensures that this can be injected even if
+     * it is empty
+     */
     @Multibinds
     Map<String, QSTileImpl<?>> tileMap();
 
-    /** A map of internal QS tile ViewModels. Ensures that this can be injected even if
-     * it is empty */
-    @Multibinds
-    Map<String, QSTileViewModel> tileViewModelMap();
-
     @Provides
     @SysUISingleton
     static AutoTileManager provideAutoTileManager(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
index d09b210..0995dd4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
@@ -24,13 +24,11 @@
 import android.widget.ImageView
 import android.widget.LinearLayout
 import android.widget.TextView
-import androidx.core.view.isInvisible
 import androidx.core.view.isVisible
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.res.R
 import com.android.systemui.animation.Expandable
 import com.android.systemui.common.ui.binder.IconViewBinder
 import com.android.systemui.dagger.SysUISingleton
@@ -40,6 +38,7 @@
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsForegroundServicesButtonViewModel
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsSecurityButtonViewModel
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
+import com.android.systemui.res.R
 import javax.inject.Inject
 import kotlin.math.roundToInt
 import kotlinx.coroutines.launch
@@ -98,10 +97,6 @@
         var previousForegroundServices: FooterActionsForegroundServicesButtonViewModel? = null
         var previousUserSwitcher: FooterActionsButtonViewModel? = null
 
-        // Set the initial visibility on the View directly so that we don't briefly show it for a
-        // few frames before [viewModel.isVisible] is collected.
-        view.isInvisible = !viewModel.isVisible.value
-
         // Listen for ViewModel updates when the View is attached.
         view.repeatWhenAttached {
             val attachedScope = this.lifecycleScope
@@ -111,12 +106,7 @@
                 // TODO(b/242040009): Should this move somewhere else?
                 launch { viewModel.observeDeviceMonitoringDialogRequests(view.context) }
 
-                // Make sure we set the correct visibility and alpha even when QS are not currently
-                // shown.
-                launch {
-                    viewModel.isVisible.collect { isVisible -> view.isInvisible = !isVisible }
-                }
-
+                // Make sure we set the correct alphas even when QS are not currently shown.
                 launch { viewModel.alpha.collect { view.alpha = it } }
                 launch {
                     viewModel.backgroundAlpha.collect {
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 eff3e76..aff4a67 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
@@ -77,14 +77,6 @@
      */
     val observeDeviceMonitoringDialogRequests: suspend (quickSettingsContext: Context) -> Unit,
 ) {
-    /**
-     * Whether the UI rendering this ViewModel should be visible. Note that even when this is false,
-     * the UI should still participate to the layout it is included in (i.e. in the View world it
-     * should be INVISIBLE, not GONE).
-     */
-    private val _isVisible = MutableStateFlow(false)
-    val isVisible: StateFlow<Boolean> = _isVisible.asStateFlow()
-
     /** The alpha the UI rendering this ViewModel should have. */
     private val _alpha = MutableStateFlow(1f)
     val alpha: StateFlow<Float> = _alpha.asStateFlow()
@@ -93,10 +85,6 @@
     private val _backgroundAlpha = MutableStateFlow(1f)
     val backgroundAlpha: StateFlow<Float> = _backgroundAlpha.asStateFlow()
 
-    fun onVisibilityChangeRequested(visible: Boolean) {
-        _isVisible.value = visible
-    }
-
     /** Called when the expansion of the Quick Settings changed. */
     fun onQuickSettingsExpansionChanged(expansion: Float, isInSplitShade: Boolean) {
         if (isInSplitShade) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/DisabledByPolicyInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/DisabledByPolicyInteractor.kt
index 056f967..d1f8945 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/DisabledByPolicyInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/DisabledByPolicyInteractor.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.qs.tiles.base.interactor
 
 import android.content.Context
+import android.os.UserHandle
 import androidx.annotation.VisibleForTesting
 import androidx.annotation.WorkerThread
 import com.android.settingslib.RestrictedLockUtils
@@ -32,7 +33,7 @@
 
 /**
  * Provides restrictions data for the tiles. This is used in
- * [com.android.systemui.qs.tiles.base.viewmodel.BaseQSTileViewModel] to determine if the tile is
+ * [com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelImpl] to determine if the tile is
  * disabled based on the [com.android.systemui.qs.tiles.viewmodel.QSTileConfig.policy].
  */
 interface DisabledByPolicyInteractor {
@@ -41,7 +42,7 @@
      * Checks if the tile is restricted by the policy for a specific user. Pass the result to the
      * [handlePolicyResult] to let the user know that the tile is disable by the admin.
      */
-    suspend fun isDisabled(userId: Int, userRestriction: String?): PolicyResult
+    suspend fun isDisabled(user: UserHandle, userRestriction: String?): PolicyResult
 
     /**
      * Returns true when [policyResult] is [PolicyResult.TileDisabled] and has been handled by this
@@ -75,14 +76,14 @@
     @Background private val backgroundDispatcher: CoroutineDispatcher,
 ) : DisabledByPolicyInteractor {
 
-    override suspend fun isDisabled(userId: Int, userRestriction: String?): PolicyResult =
+    override suspend fun isDisabled(user: UserHandle, userRestriction: String?): PolicyResult =
         withContext(backgroundDispatcher) {
             val admin: EnforcedAdmin =
-                restrictedLockProxy.getEnforcedAdmin(userId, userRestriction)
+                restrictedLockProxy.getEnforcedAdmin(user.identifier, userRestriction)
                     ?: return@withContext PolicyResult.TileEnabled
 
             return@withContext if (
-                !restrictedLockProxy.hasBaseUserRestriction(userId, userRestriction)
+                !restrictedLockProxy.hasBaseUserRestriction(user.identifier, userRestriction)
             ) {
                 PolicyResult.TileDisabled(admin)
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileDataInteractor.kt
index a3e3850..9752fea 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileDataInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileDataInteractor.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.qs.tiles.base.interactor
 
+import android.os.UserHandle
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.flow.Flow
 
@@ -29,13 +30,12 @@
 
     /**
      * Returns a data flow scoped to the user. This means the subscription will live when the tile
-     * is listened for the [userId]. It's cancelled when the tile is not listened or the user
-     * changes.
+     * is listened for the [user]. It's cancelled when the tile is not listened or the user changes.
      *
      * You can use [Flow.onStart] on the returned to update the tile with the current state as soon
      * as possible.
      */
-    fun tileData(userId: Int, triggers: Flow<DataUpdateTrigger>): Flow<DATA_TYPE>
+    fun tileData(user: UserHandle, triggers: Flow<DataUpdateTrigger>): Flow<DATA_TYPE>
 
     /**
      * Returns tile availability - whether this device currently supports this tile.
@@ -43,5 +43,5 @@
      * You can use [Flow.onStart] on the returned to update the tile with the current state as soon
      * as possible.
      */
-    fun availability(userId: Int): Flow<Boolean>
+    fun availability(user: UserHandle): Flow<Boolean>
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileInput.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileInput.kt
index 102fa36..77ff609 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileInput.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileInput.kt
@@ -16,11 +16,12 @@
 
 package com.android.systemui.qs.tiles.base.interactor
 
+import android.os.UserHandle
 import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
 
 /** @see QSTileUserActionInteractor.handleInput */
 data class QSTileInput<T>(
-    val userId: Int,
+    val user: UserHandle,
     val action: QSTileUserAction,
     val data: T,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/BaseQSTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/BaseQSTileViewModel.kt
deleted file mode 100644
index 14de5eb..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/BaseQSTileViewModel.kt
+++ /dev/null
@@ -1,308 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs.tiles.base.viewmodel
-
-import androidx.annotation.CallSuper
-import androidx.annotation.VisibleForTesting
-import com.android.internal.util.Preconditions
-import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
-import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
-import com.android.systemui.qs.tiles.base.interactor.DisabledByPolicyInteractor
-import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
-import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
-import com.android.systemui.qs.tiles.base.interactor.QSTileInput
-import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
-import com.android.systemui.qs.tiles.base.logging.QSTileLogger
-import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
-import com.android.systemui.qs.tiles.viewmodel.QSTileLifecycle
-import com.android.systemui.qs.tiles.viewmodel.QSTilePolicy
-import com.android.systemui.qs.tiles.viewmodel.QSTileState
-import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
-import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel
-import com.android.systemui.user.data.repository.UserRepository
-import com.android.systemui.util.kotlin.throttle
-import com.android.systemui.util.time.SystemClock
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.cancelChildren
-import kotlinx.coroutines.channels.BufferOverflow
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharedFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.cancellable
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.mapNotNull
-import kotlinx.coroutines.flow.merge
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.shareIn
-import kotlinx.coroutines.flow.stateIn
-
-/**
- * Provides a hassle-free way to implement new tiles according to current System UI architecture
- * standards. THis ViewModel is cheap to instantiate and does nothing until it's moved to
- * [QSTileLifecycle.ALIVE] state.
- *
- * Inject [BaseQSTileViewModel.Factory] to create a new instance of this class.
- */
-@OptIn(ExperimentalCoroutinesApi::class)
-class BaseQSTileViewModel<DATA_TYPE>
-@VisibleForTesting
-constructor(
-    override val config: QSTileConfig,
-    private val userActionInteractor: QSTileUserActionInteractor<DATA_TYPE>,
-    private val tileDataInteractor: QSTileDataInteractor<DATA_TYPE>,
-    private val mapper: QSTileDataToStateMapper<DATA_TYPE>,
-    private val disabledByPolicyInteractor: DisabledByPolicyInteractor,
-    userRepository: UserRepository,
-    private val falsingManager: FalsingManager,
-    private val qsTileAnalytics: QSTileAnalytics,
-    private val qsTileLogger: QSTileLogger,
-    private val systemClock: SystemClock,
-    private val backgroundDispatcher: CoroutineDispatcher,
-    private val tileScope: CoroutineScope,
-) : QSTileViewModel {
-
-    @AssistedInject
-    constructor(
-        @Assisted config: QSTileConfig,
-        @Assisted userActionInteractor: QSTileUserActionInteractor<DATA_TYPE>,
-        @Assisted tileDataInteractor: QSTileDataInteractor<DATA_TYPE>,
-        @Assisted mapper: QSTileDataToStateMapper<DATA_TYPE>,
-        disabledByPolicyInteractor: DisabledByPolicyInteractor,
-        userRepository: UserRepository,
-        falsingManager: FalsingManager,
-        qsTileAnalytics: QSTileAnalytics,
-        qsTileLogger: QSTileLogger,
-        systemClock: SystemClock,
-        @Background backgroundDispatcher: CoroutineDispatcher,
-    ) : this(
-        config,
-        userActionInteractor,
-        tileDataInteractor,
-        mapper,
-        disabledByPolicyInteractor,
-        userRepository,
-        falsingManager,
-        qsTileAnalytics,
-        qsTileLogger,
-        systemClock,
-        backgroundDispatcher,
-        CoroutineScope(SupervisorJob())
-    )
-
-    private val userIds: MutableStateFlow<Int> =
-        MutableStateFlow(userRepository.getSelectedUserInfo().id)
-    private val userInputs: MutableSharedFlow<QSTileUserAction> =
-        MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
-    private val forceUpdates: MutableSharedFlow<Unit> =
-        MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
-    private val spec
-        get() = config.tileSpec
-
-    private lateinit var tileData: SharedFlow<DATA_TYPE>
-
-    override lateinit var state: SharedFlow<QSTileState>
-    override val isAvailable: StateFlow<Boolean> =
-        userIds
-            .flatMapLatest { tileDataInteractor.availability(it) }
-            .flowOn(backgroundDispatcher)
-            .stateIn(
-                tileScope,
-                SharingStarted.WhileSubscribed(),
-                true,
-            )
-
-    private var currentLifeState: QSTileLifecycle = QSTileLifecycle.DEAD
-
-    @CallSuper
-    override fun forceUpdate() {
-        Preconditions.checkState(currentLifeState == QSTileLifecycle.ALIVE)
-        forceUpdates.tryEmit(Unit)
-    }
-
-    @CallSuper
-    override fun onUserIdChanged(userId: Int) {
-        Preconditions.checkState(currentLifeState == QSTileLifecycle.ALIVE)
-        userIds.tryEmit(userId)
-    }
-
-    @CallSuper
-    override fun onActionPerformed(userAction: QSTileUserAction) {
-        Preconditions.checkState(currentLifeState == QSTileLifecycle.ALIVE)
-
-        qsTileLogger.logUserAction(
-            userAction,
-            spec,
-            tileData.replayCache.isNotEmpty(),
-            state.replayCache.isNotEmpty()
-        )
-        userInputs.tryEmit(userAction)
-    }
-
-    @CallSuper
-    override fun onLifecycle(lifecycle: QSTileLifecycle) {
-        when (lifecycle) {
-            QSTileLifecycle.ALIVE -> {
-                Preconditions.checkState(currentLifeState == QSTileLifecycle.DEAD)
-                tileData = createTileDataFlow()
-                state =
-                    tileData
-                        .map { data ->
-                            mapper.map(config, data).also { state ->
-                                qsTileLogger.logStateUpdate(spec, state, data)
-                            }
-                        }
-                        .flowOn(backgroundDispatcher)
-                        .shareIn(
-                            tileScope,
-                            SharingStarted.WhileSubscribed(),
-                            replay = 1,
-                        )
-            }
-            QSTileLifecycle.DEAD -> {
-                Preconditions.checkState(currentLifeState == QSTileLifecycle.ALIVE)
-                tileScope.coroutineContext.cancelChildren()
-            }
-        }
-        currentLifeState = lifecycle
-    }
-
-    private fun createTileDataFlow(): SharedFlow<DATA_TYPE> =
-        userIds
-            .flatMapLatest { userId ->
-                val updateTriggers =
-                    merge(
-                            userInputFlow(userId),
-                            forceUpdates
-                                .map { DataUpdateTrigger.ForceUpdate }
-                                .onEach { qsTileLogger.logForceUpdate(spec) },
-                        )
-                        .onStart {
-                            emit(DataUpdateTrigger.InitialRequest)
-                            qsTileLogger.logInitialRequest(spec)
-                        }
-                tileDataInteractor
-                    .tileData(userId, updateTriggers)
-                    .cancellable()
-                    .flowOn(backgroundDispatcher)
-            }
-            .shareIn(
-                tileScope,
-                SharingStarted.WhileSubscribed(),
-                replay = 1, // we only care about the most recent value
-            )
-
-    /**
-     * Creates a user input flow which:
-     * - filters false inputs with [falsingManager]
-     * - takes care of a tile being disable by policy using [disabledByPolicyInteractor]
-     * - notifies [userActionInteractor] about the action
-     * - logs it accordingly using [qsTileLogger] and [qsTileAnalytics]
-     *
-     * Subscribing to the result flow twice will result in doubling all actions, logs and analytics.
-     */
-    private fun userInputFlow(userId: Int): Flow<DataUpdateTrigger> {
-        return userInputs
-            .filterFalseActions()
-            .filterByPolicy(userId)
-            .throttle(CLICK_THROTTLE_DURATION, systemClock)
-            // Skip the input until there is some data
-            .mapNotNull { action ->
-                val state: QSTileState = state.replayCache.lastOrNull() ?: return@mapNotNull null
-                val data: DATA_TYPE = tileData.replayCache.lastOrNull() ?: return@mapNotNull null
-                qsTileLogger.logUserActionPipeline(spec, action, state, data)
-                qsTileAnalytics.trackUserAction(config, action)
-
-                DataUpdateTrigger.UserInput(QSTileInput(userId, action, data))
-            }
-            .onEach { userActionInteractor.handleInput(it.input) }
-            .flowOn(backgroundDispatcher)
-    }
-
-    private fun Flow<QSTileUserAction>.filterByPolicy(userId: Int): Flow<QSTileUserAction> =
-        when (config.policy) {
-            is QSTilePolicy.NoRestrictions -> this
-            is QSTilePolicy.Restricted ->
-                filter { action ->
-                    val result =
-                        disabledByPolicyInteractor.isDisabled(userId, config.policy.userRestriction)
-                    !disabledByPolicyInteractor.handlePolicyResult(result).also { isDisabled ->
-                        if (isDisabled) {
-                            qsTileLogger.logUserActionRejectedByPolicy(action, spec)
-                        }
-                    }
-                }
-        }
-
-    private fun Flow<QSTileUserAction>.filterFalseActions(): Flow<QSTileUserAction> =
-        filter { action ->
-            val isFalseAction =
-                when (action) {
-                    is QSTileUserAction.Click ->
-                        falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)
-                    is QSTileUserAction.LongClick ->
-                        falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)
-                }
-            if (isFalseAction) {
-                qsTileLogger.logUserActionRejectedByFalsing(action, spec)
-            }
-            !isFalseAction
-        }
-
-    private companion object {
-        const val CLICK_THROTTLE_DURATION = 200L
-    }
-
-    /**
-     * Factory interface for assisted inject. Dagger has bad time supporting generics in assisted
-     * injection factories now. That's why you need to create an interface implementing this one and
-     * annotate it with [dagger.assisted.AssistedFactory].
-     *
-     * ex: @AssistedFactory interface FooFactory : BaseQSTileViewModel.Factory<FooData>
-     */
-    interface Factory<T> {
-
-        /**
-         * @param config contains all the static information (like TileSpec) about the tile.
-         * @param userActionInteractor encapsulates user input processing logic. Use it to start
-         *   activities, show dialogs or otherwise update the tile state.
-         * @param tileDataInteractor provides [DATA_TYPE] and its availability.
-         * @param mapper maps [DATA_TYPE] to the [QSTileState] that is then displayed by the View
-         *   layer. It's called in [backgroundDispatcher], so it's safe to perform long running
-         *   operations there.
-         */
-        fun create(
-            config: QSTileConfig,
-            userActionInteractor: QSTileUserActionInteractor<T>,
-            tileDataInteractor: QSTileDataInteractor<T>,
-            mapper: QSTileDataToStateMapper<T>,
-        ): BaseQSTileViewModel<T>
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt
new file mode 100644
index 0000000..736f7cf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.tiles.base.viewmodel
+
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
+import com.android.systemui.qs.tiles.base.interactor.DisabledByPolicyInteractor
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.base.logging.QSTileLogger
+import com.android.systemui.qs.tiles.impl.di.QSTileComponent
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProvider
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.util.time.SystemClock
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+
+/**
+ * Factory to create an appropriate [QSTileViewModelImpl] instance depending on your circumstances.
+ *
+ * @see [QSTileViewModelFactory.Component]
+ * @see [QSTileViewModelFactory.Static]
+ */
+sealed interface QSTileViewModelFactory<T> {
+
+    /**
+     * This factory allows you to pass an instance of [QSTileComponent] to a view model effectively
+     * binding them together. This achieves a DI scope that lives along the instance of
+     * [QSTileViewModelImpl].
+     */
+    class Component<T>
+    @Inject
+    constructor(
+        private val disabledByPolicyInteractor: DisabledByPolicyInteractor,
+        private val userRepository: UserRepository,
+        private val falsingManager: FalsingManager,
+        private val qsTileAnalytics: QSTileAnalytics,
+        private val qsTileLogger: QSTileLogger,
+        private val qsTileConfigProvider: QSTileConfigProvider,
+        private val systemClock: SystemClock,
+        @Background private val backgroundDispatcher: CoroutineDispatcher,
+    ) : QSTileViewModelFactory<T> {
+
+        /**
+         * Creates [QSTileViewModelImpl] based on the interactors obtained from [component].
+         * Reference of that [component] is then stored along the view model.
+         */
+        fun create(tileSpec: TileSpec, component: QSTileComponent<T>): QSTileViewModelImpl<T> =
+            QSTileViewModelImpl(
+                qsTileConfigProvider.getConfig(tileSpec.spec),
+                component::userActionInteractor,
+                component::dataInteractor,
+                component::dataToStateMapper,
+                disabledByPolicyInteractor,
+                userRepository,
+                falsingManager,
+                qsTileAnalytics,
+                qsTileLogger,
+                systemClock,
+                backgroundDispatcher,
+            )
+    }
+
+    /**
+     * This factory passes by necessary implementations to the [QSTileViewModelImpl]. This is a
+     * default choice for most of the tiles.
+     */
+    class Static<T>
+    @Inject
+    constructor(
+        private val disabledByPolicyInteractor: DisabledByPolicyInteractor,
+        private val userRepository: UserRepository,
+        private val falsingManager: FalsingManager,
+        private val qsTileAnalytics: QSTileAnalytics,
+        private val qsTileLogger: QSTileLogger,
+        private val qsTileConfigProvider: QSTileConfigProvider,
+        private val systemClock: SystemClock,
+        @Background private val backgroundDispatcher: CoroutineDispatcher,
+    ) : QSTileViewModelFactory<T> {
+
+        /**
+         * @param tileSpec of the created tile.
+         * @param userActionInteractor encapsulates user input processing logic. Use it to start
+         *   activities, show dialogs or otherwise update the tile state.
+         * @param tileDataInteractor provides [DATA_TYPE] and its availability.
+         * @param mapper maps [DATA_TYPE] to the [QSTileState] that is then displayed by the View
+         *   layer. It's called in [backgroundDispatcher], so it's safe to perform long running
+         *   operations there.
+         */
+        fun create(
+            tileSpec: TileSpec,
+            userActionInteractor: QSTileUserActionInteractor<T>,
+            tileDataInteractor: QSTileDataInteractor<T>,
+            mapper: QSTileDataToStateMapper<T>,
+        ): QSTileViewModelImpl<T> =
+            QSTileViewModelImpl(
+                qsTileConfigProvider.getConfig(tileSpec.spec),
+                { userActionInteractor },
+                { tileDataInteractor },
+                { mapper },
+                disabledByPolicyInteractor,
+                userRepository,
+                falsingManager,
+                qsTileAnalytics,
+                qsTileLogger,
+                systemClock,
+                backgroundDispatcher,
+            )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
new file mode 100644
index 0000000..0bee48f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
@@ -0,0 +1,227 @@
+/*
+ * 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.tiles.base.viewmodel
+
+import android.os.UserHandle
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.base.interactor.DisabledByPolicyInteractor
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.base.interactor.QSTileInput
+import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.base.logging.QSTileLogger
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTilePolicy
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel
+import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.util.kotlin.throttle
+import com.android.systemui.util.time.SystemClock
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.cancellable
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * Provides a hassle-free way to implement new tiles according to current System UI architecture
+ * standards. This ViewModel is cheap to instantiate and does nothing until its [state] is listened.
+ *
+ * Don't use this constructor directly. Instead, inject [QSTileViewModelFactory] to create a new
+ * instance of this class.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+class QSTileViewModelImpl<DATA_TYPE>(
+    override val config: QSTileConfig,
+    private val userActionInteractor: () -> QSTileUserActionInteractor<DATA_TYPE>,
+    private val tileDataInteractor: () -> QSTileDataInteractor<DATA_TYPE>,
+    private val mapper: () -> QSTileDataToStateMapper<DATA_TYPE>,
+    private val disabledByPolicyInteractor: DisabledByPolicyInteractor,
+    userRepository: UserRepository,
+    private val falsingManager: FalsingManager,
+    private val qsTileAnalytics: QSTileAnalytics,
+    private val qsTileLogger: QSTileLogger,
+    private val systemClock: SystemClock,
+    private val backgroundDispatcher: CoroutineDispatcher,
+    private val tileScope: CoroutineScope = CoroutineScope(SupervisorJob()),
+) : QSTileViewModel {
+
+    private val users: MutableStateFlow<UserHandle> =
+        MutableStateFlow(userRepository.getSelectedUserInfo().userHandle)
+    private val userInputs: MutableSharedFlow<QSTileUserAction> =
+        MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
+    private val forceUpdates: MutableSharedFlow<Unit> =
+        MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
+    private val spec
+        get() = config.tileSpec
+
+    private val tileData: SharedFlow<DATA_TYPE> = createTileDataFlow()
+
+    override val state: SharedFlow<QSTileState> =
+        tileData
+            .map { data ->
+                mapper().map(config, data).also { state ->
+                    qsTileLogger.logStateUpdate(spec, state, data)
+                }
+            }
+            .flowOn(backgroundDispatcher)
+            .shareIn(
+                tileScope,
+                SharingStarted.WhileSubscribed(),
+                replay = 1,
+            )
+    override val isAvailable: StateFlow<Boolean> =
+        users
+            .flatMapLatest { tileDataInteractor().availability(it) }
+            .flowOn(backgroundDispatcher)
+            .stateIn(
+                tileScope,
+                SharingStarted.WhileSubscribed(),
+                true,
+            )
+
+    override fun forceUpdate() {
+        forceUpdates.tryEmit(Unit)
+    }
+
+    override fun onUserChanged(user: UserHandle) {
+        users.tryEmit(user)
+    }
+
+    override fun onActionPerformed(userAction: QSTileUserAction) {
+        qsTileLogger.logUserAction(
+            userAction,
+            spec,
+            tileData.replayCache.isNotEmpty(),
+            state.replayCache.isNotEmpty()
+        )
+        userInputs.tryEmit(userAction)
+    }
+
+    override fun destroy() {
+        tileScope.cancel()
+    }
+
+    private fun createTileDataFlow(): SharedFlow<DATA_TYPE> =
+        users
+            .flatMapLatest { user ->
+                val updateTriggers =
+                    merge(
+                            userInputFlow(user),
+                            forceUpdates
+                                .map { DataUpdateTrigger.ForceUpdate }
+                                .onEach { qsTileLogger.logForceUpdate(spec) },
+                        )
+                        .onStart {
+                            emit(DataUpdateTrigger.InitialRequest)
+                            qsTileLogger.logInitialRequest(spec)
+                        }
+                tileDataInteractor()
+                    .tileData(user, updateTriggers)
+                    .cancellable()
+                    .flowOn(backgroundDispatcher)
+            }
+            .shareIn(
+                tileScope,
+                SharingStarted.WhileSubscribed(),
+                replay = 1, // we only care about the most recent value
+            )
+
+    /**
+     * Creates a user input flow which:
+     * - filters false inputs with [falsingManager]
+     * - takes care of a tile being disable by policy using [disabledByPolicyInteractor]
+     * - notifies [userActionInteractor] about the action
+     * - logs it accordingly using [qsTileLogger] and [qsTileAnalytics]
+     *
+     * Subscribing to the result flow twice will result in doubling all actions, logs and analytics.
+     */
+    private fun userInputFlow(user: UserHandle): Flow<DataUpdateTrigger> {
+        return userInputs
+            .filterFalseActions()
+            .filterByPolicy(user)
+            .throttle(CLICK_THROTTLE_DURATION, systemClock)
+            // Skip the input until there is some data
+            .mapNotNull { action ->
+                val state: QSTileState = state.replayCache.lastOrNull() ?: return@mapNotNull null
+                val data: DATA_TYPE = tileData.replayCache.lastOrNull() ?: return@mapNotNull null
+                qsTileLogger.logUserActionPipeline(spec, action, state, data)
+                qsTileAnalytics.trackUserAction(config, action)
+
+                DataUpdateTrigger.UserInput(QSTileInput(user, action, data))
+            }
+            .onEach { userActionInteractor().handleInput(it.input) }
+            .flowOn(backgroundDispatcher)
+    }
+
+    private fun Flow<QSTileUserAction>.filterByPolicy(user: UserHandle): Flow<QSTileUserAction> =
+        config.policy.let { policy ->
+            when (policy) {
+                is QSTilePolicy.NoRestrictions -> this@filterByPolicy
+                is QSTilePolicy.Restricted ->
+                    filter { action ->
+                        val result =
+                            disabledByPolicyInteractor.isDisabled(user, policy.userRestriction)
+                        !disabledByPolicyInteractor.handlePolicyResult(result).also { isDisabled ->
+                            if (isDisabled) {
+                                qsTileLogger.logUserActionRejectedByPolicy(action, spec)
+                            }
+                        }
+                    }
+            }
+        }
+
+    private fun Flow<QSTileUserAction>.filterFalseActions(): Flow<QSTileUserAction> =
+        filter { action ->
+            val isFalseAction =
+                when (action) {
+                    is QSTileUserAction.Click ->
+                        falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)
+                    is QSTileUserAction.LongClick ->
+                        falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)
+                }
+            if (isFalseAction) {
+                qsTileLogger.logUserActionRejectedByFalsing(action, spec)
+            }
+            !isFalseAction
+        }
+
+    private companion object {
+        const val CLICK_THROTTLE_DURATION = 200L
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/di/NewQSTileFactory.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/di/NewQSTileFactory.kt
index d0809c5..7d7af64 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/di/NewQSTileFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/di/NewQSTileFactory.kt
@@ -19,7 +19,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.plugins.qs.QSFactory
 import com.android.systemui.plugins.qs.QSTile
-import com.android.systemui.qs.tiles.viewmodel.QSTileLifecycle
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProvider
 import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel
 import com.android.systemui.qs.tiles.viewmodel.QSTileViewModelAdapter
 import javax.inject.Inject
@@ -30,15 +30,22 @@
 class NewQSTileFactory
 @Inject
 constructor(
+    qsTileConfigProvider: QSTileConfigProvider,
     private val adapterFactory: QSTileViewModelAdapter.Factory,
     private val tileMap:
         Map<String, @JvmSuppressWildcards Provider<@JvmSuppressWildcards QSTileViewModel>>,
 ) : QSFactory {
 
+    init {
+        for (viewModelTileSpec in tileMap.keys) {
+            // throws an exception when there is no config for a tileSpec of an injected viewModel
+            qsTileConfigProvider.getConfig(viewModelTileSpec)
+        }
+    }
+
     override fun createTile(tileSpec: String): QSTile? =
         tileMap[tileSpec]?.let {
             val tile = it.get()
-            tile.onLifecycle(QSTileLifecycle.ALIVE)
             adapterFactory.create(tile)
         }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/di/QSTilesModule.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/di/QSTilesModule.kt
new file mode 100644
index 0000000..32522ad
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/di/QSTilesModule.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.tiles.di
+
+import com.android.systemui.qs.tiles.impl.custom.di.CustomTileComponent
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProvider
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProviderImpl
+import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.Multibinds
+
+/** Module listing subcomponents */
+@Module(
+    subcomponents =
+        [
+            CustomTileComponent::class,
+        ]
+)
+interface QSTilesModule {
+
+    /**
+     * A map of internal QS tile ViewModels. Ensures that this can be injected even if it is empty
+     */
+    @Multibinds fun tileViewModelConfigs(): Map<String, QSTileConfig>
+
+    /**
+     * A map of internal QS tile ViewModels. Ensures that this can be injected even if it is empty
+     */
+    @Multibinds fun tileViewModelMap(): Map<String, QSTileViewModel>
+
+    @Binds fun bindQSTileConfigProvider(impl: QSTileConfigProviderImpl): QSTileConfigProvider
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractor.kt
index 8957fc3..9c63a30 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractor.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.qs.tiles.dialog.bluetooth
 
+import android.bluetooth.BluetoothAdapter
 import android.bluetooth.BluetoothAdapter.STATE_OFF
 import android.bluetooth.BluetoothAdapter.STATE_ON
 import com.android.settingslib.bluetooth.BluetoothCallback
@@ -37,6 +38,7 @@
 @Inject
 constructor(
     private val localBluetoothManager: LocalBluetoothManager?,
+    private val logger: BluetoothTileDialogLogger,
     @Application private val coroutineScope: CoroutineScope,
 ) {
 
@@ -47,6 +49,10 @@
                         override fun onBluetoothStateChanged(bluetoothState: Int) {
                             if (bluetoothState == STATE_ON || bluetoothState == STATE_OFF) {
                                 super.onBluetoothStateChanged(bluetoothState)
+                                logger.logBluetoothState(
+                                    BluetoothStateStage.BLUETOOTH_STATE_CHANGE_RECEIVED,
+                                    BluetoothAdapter.nameForState(bluetoothState)
+                                )
                                 trySendWithFailureLogging(
                                     bluetoothState == STATE_ON,
                                     TAG,
@@ -70,6 +76,10 @@
             if (isBluetoothEnabled != value) {
                 localBluetoothManager?.bluetoothAdapter?.apply {
                     if (value) enable() else disable()
+                    logger.logBluetoothState(
+                        BluetoothStateStage.BLUETOOTH_STATE_VALUE_SET,
+                        value.toString()
+                    )
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt
index 80af76d..4096226 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt
@@ -34,6 +34,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.util.time.SystemClock
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asSharedFlow
@@ -46,7 +47,9 @@
     private val bluetoothToggleInitialValue: Boolean,
     private val subtitleResIdInitialValue: Int,
     private val bluetoothTileDialogCallback: BluetoothTileDialogCallback,
+    private val systemClock: SystemClock,
     private val uiEventLogger: UiEventLogger,
+    private val logger: BluetoothTileDialogLogger,
     context: Context,
 ) : SystemUIDialog(context, DEFAULT_THEME, DEFAULT_DISMISS_ON_DEVICE_LOCK) {
 
@@ -102,9 +105,11 @@
         showSeeAll: Boolean,
         showPairNewDevice: Boolean
     ) {
+        val start = systemClock.elapsedRealtime()
         deviceItemAdapter.refreshDeviceItemList(deviceItem) {
             seeAllViewGroup.visibility = if (showSeeAll) VISIBLE else GONE
             pairNewDeviceViewGroup.visibility = if (showPairNewDevice) VISIBLE else GONE
+            logger.logDeviceUiUpdate(systemClock.elapsedRealtime() - start)
         }
     }
 
@@ -117,6 +122,7 @@
         toggleView.isChecked = bluetoothToggleInitialValue
         toggleView.setOnCheckedChangeListener { _, isChecked ->
             mutableBluetoothStateToggle.value = isChecked
+            logger.logBluetoothState(BluetoothStateStage.USER_TOGGLED, isChecked.toString())
             uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_TOGGLE_CLICKED)
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogLogger.kt
new file mode 100644
index 0000000..5d18dc1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogLogger.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.tiles.dialog.bluetooth
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel.DEBUG
+import com.android.systemui.log.dagger.BluetoothTileDialogLog
+import javax.inject.Inject
+
+private const val TAG = "BluetoothTileDialogLog"
+
+enum class BluetoothStateStage {
+    USER_TOGGLED,
+    BLUETOOTH_STATE_VALUE_SET,
+    BLUETOOTH_STATE_CHANGE_RECEIVED
+}
+
+enum class DeviceFetchTrigger {
+    FIRST_LOAD,
+    BLUETOOTH_STATE_CHANGE_RECEIVED,
+    BLUETOOTH_CALLBACK_RECEIVED
+}
+
+enum class JobStatus {
+    FINISHED,
+    CANCELLED
+}
+
+class BluetoothTileDialogLogger
+@Inject
+constructor(@BluetoothTileDialogLog private val logBuffer: LogBuffer) {
+
+    fun logBluetoothState(stage: BluetoothStateStage, state: String) =
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = stage.toString()
+                str2 = state
+            },
+            { "BluetoothState. stage=$str1 state=$str2" }
+        )
+
+    fun logDeviceClick(address: String, type: DeviceItemType) =
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = address
+                str2 = type.toString()
+            },
+            { "DeviceClick. address=$str1 type=$str2" }
+        )
+
+    fun logActiveDeviceChanged(address: String?, profileId: Int) =
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = address
+                int1 = profileId
+            },
+            { "ActiveDeviceChanged. address=$str1 profileId=$int1" }
+        )
+
+    fun logProfileConnectionStateChanged(address: String, state: String, profileId: Int) =
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = address
+                str2 = state
+                int1 = profileId
+            },
+            { "ProfileConnectionStateChanged. address=$str1 state=$str2 profileId=$int1" }
+        )
+
+    fun logDeviceFetch(status: JobStatus, trigger: DeviceFetchTrigger, duration: Long) =
+        logBuffer.log(
+            TAG,
+            DEBUG,
+            {
+                str1 = status.toString()
+                str2 = trigger.toString()
+                long1 = duration
+            },
+            { "DeviceFetch. status=$str1 trigger=$str2 duration=$long1" }
+        )
+
+    fun logDeviceUiUpdate(duration: Long) =
+        logBuffer.log(TAG, DEBUG, { long1 = duration }, { "DeviceUiUpdate. duration=$long1" })
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt
index 2865ad7..86e5dde 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt
@@ -28,7 +28,10 @@
     @UiEvent(doc = "Gear icon clicked") DEVICE_GEAR_CLICKED(1497),
     @UiEvent(doc = "Device clicked") DEVICE_CLICKED(1498),
     @UiEvent(doc = "Connected device clicked to active") CONNECTED_DEVICE_SET_ACTIVE(1499),
-    @UiEvent(doc = "Saved clicked to connect") SAVED_DEVICE_CONNECT(1500);
+    @UiEvent(doc = "Saved clicked to connect") SAVED_DEVICE_CONNECT(1500),
+    @UiEvent(doc = "Active device clicked to disconnect") ACTIVE_DEVICE_DISCONNECT(1507),
+    @UiEvent(doc = "Connected other device clicked to disconnect")
+    CONNECTED_OTHER_DEVICE_DISCONNECT(1508);
 
     override fun getId() = metricId
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
index 8e27493..f7e0de3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
@@ -35,10 +35,12 @@
 import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialog.Companion.MAX_DEVICE_ITEM_ENTRY
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.util.time.SystemClock
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
@@ -53,7 +55,9 @@
     private val bluetoothStateInteractor: BluetoothStateInteractor,
     private val dialogLaunchAnimator: DialogLaunchAnimator,
     private val activityStarter: ActivityStarter,
+    private val systemClock: SystemClock,
     private val uiEventLogger: UiEventLogger,
+    private val logger: BluetoothTileDialogLogger,
     @Application private val coroutineScope: CoroutineScope,
     @Main private val mainDispatcher: CoroutineDispatcher,
 ) : BluetoothTileDialogCallback {
@@ -90,7 +94,11 @@
                 }
                     ?: dialog!!.show()
                 updateDeviceItemJob?.cancel()
-                updateDeviceItemJob = launch { deviceItemInteractor.updateDeviceItems(context) }
+                updateDeviceItemJob = launch {
+                    // Add a slight delay for smoother dialog bounds change
+                    delay(FIRST_LOAD_DELAY_MS)
+                    deviceItemInteractor.updateDeviceItems(context, DeviceFetchTrigger.FIRST_LOAD)
+                }
 
                 bluetoothStateInteractor.bluetoothStateUpdate
                     .filterNotNull()
@@ -98,7 +106,10 @@
                         dialog!!.onBluetoothStateUpdated(it, getSubtitleResId(it))
                         updateDeviceItemJob?.cancel()
                         updateDeviceItemJob = launch {
-                            deviceItemInteractor.updateDeviceItems(context)
+                            deviceItemInteractor.updateDeviceItems(
+                                context,
+                                DeviceFetchTrigger.BLUETOOTH_STATE_CHANGE_RECEIVED
+                            )
                         }
                     }
                     .launchIn(this)
@@ -107,7 +118,10 @@
                     .onEach {
                         updateDeviceItemJob?.cancel()
                         updateDeviceItemJob = launch {
-                            deviceItemInteractor.updateDeviceItems(context)
+                            deviceItemInteractor.updateDeviceItems(
+                                context,
+                                DeviceFetchTrigger.BLUETOOTH_CALLBACK_RECEIVED
+                            )
                         }
                     }
                     .launchIn(this)
@@ -139,7 +153,9 @@
                 bluetoothStateInteractor.isBluetoothEnabled,
                 getSubtitleResId(bluetoothStateInteractor.isBluetoothEnabled),
                 this@BluetoothTileDialogViewModel,
+                systemClock,
                 uiEventLogger,
+                logger,
                 context
             )
             .apply { SystemUIDialog.registerDismissListener(this) { dismissDialog() } }
@@ -189,6 +205,7 @@
 
     companion object {
         private const val INTERACTION_JANK_TAG = "bluetooth_tile_dialog"
+        private const val FIRST_LOAD_DELAY_MS = 500L
         private fun getSubtitleResId(isBluetoothEnabled: Boolean) =
             if (isBluetoothEnabled) R.string.quick_settings_bluetooth_tile_subtitle
             else R.string.bt_is_off
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItem.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItem.kt
index 50eaf38..2c8d2a0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItem.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItem.kt
@@ -36,6 +36,7 @@
 import com.android.settingslib.bluetooth.CachedBluetoothDevice
 
 enum class DeviceItemType {
+    ACTIVE_MEDIA_BLUETOOTH_DEVICE,
     AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
     CONNECTED_BLUETOOTH_DEVICE,
     SAVED_BLUETOOTH_DEVICE,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactory.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactory.kt
index 8c22614..7bb1619 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactory.kt
@@ -39,12 +39,38 @@
     abstract fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem
 }
 
+internal class ActiveMediaDeviceItemFactory : DeviceItemFactory() {
+    override fun isFilterMatched(
+        cachedDevice: CachedBluetoothDevice,
+        audioManager: AudioManager?
+    ): Boolean {
+        return BluetoothUtils.isActiveMediaDevice(cachedDevice) &&
+            BluetoothUtils.isAvailableMediaBluetoothDevice(cachedDevice, audioManager)
+    }
+
+    override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem {
+        return DeviceItem(
+            type = DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE,
+            cachedBluetoothDevice = cachedDevice,
+            deviceName = cachedDevice.name,
+            connectionSummary = cachedDevice.connectionSummary ?: "",
+            iconWithDescription =
+                BluetoothUtils.getBtClassDrawableWithDescription(context, cachedDevice).let { p ->
+                    Pair(p.first, p.second)
+                },
+            background = backgroundOn,
+            isEnabled = !cachedDevice.isBusy,
+        )
+    }
+}
+
 internal class AvailableMediaDeviceItemFactory : DeviceItemFactory() {
     override fun isFilterMatched(
         cachedDevice: CachedBluetoothDevice,
         audioManager: AudioManager?
     ): Boolean {
-        return BluetoothUtils.isAvailableMediaBluetoothDevice(cachedDevice, audioManager)
+        return !BluetoothUtils.isActiveMediaDevice(cachedDevice) &&
+            BluetoothUtils.isAvailableMediaBluetoothDevice(cachedDevice, audioManager)
     }
 
     // TODO(b/298124674): move create() to the abstract class to reduce duplicate code
@@ -59,7 +85,7 @@
                 BluetoothUtils.getBtClassDrawableWithDescription(context, cachedDevice).let { p ->
                     Pair(p.first, p.second)
                 },
-            background = backgroundOn,
+            background = if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff,
             isEnabled = !cachedDevice.isBusy,
         )
     }
@@ -84,7 +110,7 @@
                 BluetoothUtils.getBtClassDrawableWithDescription(context, cachedDevice).let { p ->
                     Pair(p.first, p.second)
                 },
-            background = backgroundOn,
+            background = if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff,
             isEnabled = !cachedDevice.isBusy,
         )
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractor.kt
index e196c6c..76fbf8e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractor.kt
@@ -22,7 +22,6 @@
 import android.media.AudioManager
 import com.android.internal.logging.UiEventLogger
 import com.android.settingslib.bluetooth.BluetoothCallback
-import com.android.settingslib.bluetooth.BluetoothUtils
 import com.android.settingslib.bluetooth.CachedBluetoothDevice
 import com.android.settingslib.bluetooth.LocalBluetoothManager
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
@@ -30,6 +29,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.util.time.SystemClock
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
@@ -39,6 +39,7 @@
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.isActive
 import kotlinx.coroutines.withContext
 
 /** Holds business logic for the Bluetooth Dialog after clicking on the Bluetooth QS tile. */
@@ -50,7 +51,9 @@
     private val audioManager: AudioManager,
     private val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter(),
     private val localBluetoothManager: LocalBluetoothManager?,
+    private val systemClock: SystemClock,
     private val uiEventLogger: UiEventLogger,
+    private val logger: BluetoothTileDialogLogger,
     @Application private val coroutineScope: CoroutineScope,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
 ) {
@@ -69,22 +72,10 @@
                             bluetoothProfile: Int
                         ) {
                             super.onActiveDeviceChanged(activeDevice, bluetoothProfile)
+                            logger.logActiveDeviceChanged(activeDevice?.address, bluetoothProfile)
                             trySendWithFailureLogging(Unit, TAG, "onActiveDeviceChanged")
                         }
 
-                        override fun onConnectionStateChanged(
-                            cachedDevice: CachedBluetoothDevice?,
-                            state: Int
-                        ) {
-                            super.onConnectionStateChanged(cachedDevice, state)
-                            trySendWithFailureLogging(Unit, TAG, "onConnectionStateChanged")
-                        }
-
-                        override fun onDeviceAdded(cachedDevice: CachedBluetoothDevice) {
-                            super.onDeviceAdded(cachedDevice)
-                            trySendWithFailureLogging(Unit, TAG, "onDeviceAdded")
-                        }
-
                         override fun onProfileConnectionStateChanged(
                             cachedDevice: CachedBluetoothDevice,
                             state: Int,
@@ -95,8 +86,24 @@
                                 state,
                                 bluetoothProfile
                             )
+                            logger.logProfileConnectionStateChanged(
+                                cachedDevice.address,
+                                state.toString(),
+                                bluetoothProfile
+                            )
                             trySendWithFailureLogging(Unit, TAG, "onProfileConnectionStateChanged")
                         }
+
+                        override fun onAclConnectionStateChanged(
+                            cachedDevice: CachedBluetoothDevice,
+                            state: Int
+                        ) {
+                            super.onAclConnectionStateChanged(cachedDevice, state)
+                            // Listen only when a device is disconnecting
+                            if (state == 0) {
+                                trySendWithFailureLogging(Unit, TAG, "onAclConnectionStateChanged")
+                            }
+                        }
                     }
                 localBluetoothManager?.eventManager?.registerCallback(listener)
                 awaitClose { localBluetoothManager?.eventManager?.unregisterCallback(listener) }
@@ -105,6 +112,7 @@
 
     private var deviceItemFactoryList: List<DeviceItemFactory> =
         listOf(
+            ActiveMediaDeviceItemFactory(),
             AvailableMediaDeviceItemFactory(),
             ConnectedDeviceItemFactory(),
             SavedDeviceItemFactory()
@@ -112,14 +120,16 @@
 
     private var displayPriority: List<DeviceItemType> =
         listOf(
+            DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE,
             DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
             DeviceItemType.CONNECTED_BLUETOOTH_DEVICE,
             DeviceItemType.SAVED_BLUETOOTH_DEVICE,
         )
 
-    internal suspend fun updateDeviceItems(context: Context) {
+    internal suspend fun updateDeviceItems(context: Context, trigger: DeviceFetchTrigger) {
         withContext(backgroundDispatcher) {
-            mutableDeviceItemUpdate.tryEmit(
+            val start = systemClock.elapsedRealtime()
+            val deviceItems =
                 bluetoothTileDialogRepository.cachedDevices
                     .mapNotNull { cachedDevice ->
                         deviceItemFactoryList
@@ -127,7 +137,22 @@
                             ?.create(context, cachedDevice)
                     }
                     .sort(displayPriority, bluetoothAdapter?.mostRecentlyConnectedDevices)
-            )
+
+            // Only emit when the job is not cancelled
+            if (isActive) {
+                mutableDeviceItemUpdate.tryEmit(deviceItems)
+                logger.logDeviceFetch(
+                    JobStatus.FINISHED,
+                    trigger,
+                    systemClock.elapsedRealtime() - start
+                )
+            } else {
+                logger.logDeviceFetch(
+                    JobStatus.CANCELLED,
+                    trigger,
+                    systemClock.elapsedRealtime() - start
+                )
+            }
         }
     }
 
@@ -144,17 +169,26 @@
     }
 
     internal fun updateDeviceItemOnClick(deviceItem: DeviceItem) {
-        when (deviceItem.type) {
-            DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE -> {
-                if (!BluetoothUtils.isActiveMediaDevice(deviceItem.cachedBluetoothDevice)) {
-                    deviceItem.cachedBluetoothDevice.setActive()
+        logger.logDeviceClick(deviceItem.cachedBluetoothDevice.address, deviceItem.type)
+
+        deviceItem.cachedBluetoothDevice.apply {
+            when (deviceItem.type) {
+                DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE -> {
+                    disconnect()
+                    uiEventLogger.log(BluetoothTileDialogUiEvent.ACTIVE_DEVICE_DISCONNECT)
+                }
+                DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE -> {
+                    setActive()
                     uiEventLogger.log(BluetoothTileDialogUiEvent.CONNECTED_DEVICE_SET_ACTIVE)
                 }
-            }
-            DeviceItemType.CONNECTED_BLUETOOTH_DEVICE -> {}
-            DeviceItemType.SAVED_BLUETOOTH_DEVICE -> {
-                deviceItem.cachedBluetoothDevice.connect()
-                uiEventLogger.log(BluetoothTileDialogUiEvent.SAVED_DEVICE_CONNECT)
+                DeviceItemType.CONNECTED_BLUETOOTH_DEVICE -> {
+                    disconnect()
+                    uiEventLogger.log(BluetoothTileDialogUiEvent.CONNECTED_OTHER_DEVICE_DISCONNECT)
+                }
+                DeviceItemType.SAVED_BLUETOOTH_DEVICE -> {
+                    connect()
+                    uiEventLogger.log(BluetoothTileDialogUiEvent.SAVED_DEVICE_CONNECT)
+                }
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileData.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileData.kt
new file mode 100644
index 0000000..bb5a229
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileData.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.qs.tiles.impl.custom
+
+import android.content.ComponentName
+import android.graphics.drawable.Icon
+import android.os.UserHandle
+import android.service.quicksettings.Tile
+import com.android.systemui.qs.tiles.impl.custom.di.bound.CustomTileBoundComponent
+
+data class CustomTileData(
+    val user: UserHandle,
+    val componentName: ComponentName,
+    val tile: Tile,
+    val callingAppUid: Int,
+    val isActive: Boolean,
+    val hasPendingBind: Boolean,
+    val shouldShowChevron: Boolean,
+    val defaultTileLabel: CharSequence?,
+    val defaultTileIcon: Icon?,
+    val component: CustomTileBoundComponent,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileInteractor.kt
new file mode 100644
index 0000000..761274e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileInteractor.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.tiles.impl.custom
+
+import android.os.UserHandle
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
+import com.android.systemui.qs.tiles.impl.di.QSTileScope
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+@QSTileScope
+class CustomTileInteractor @Inject constructor() : QSTileDataInteractor<CustomTileData> {
+
+    override fun tileData(
+        user: UserHandle,
+        triggers: Flow<DataUpdateTrigger>
+    ): Flow<CustomTileData> {
+        TODO("Not yet implemented")
+    }
+
+    override fun availability(user: UserHandle): Flow<Boolean> {
+        TODO("Not yet implemented")
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileMapper.kt
new file mode 100644
index 0000000..f7bec02
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileMapper.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.qs.tiles.impl.custom
+
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.impl.di.QSTileScope
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import javax.inject.Inject
+
+@QSTileScope
+class CustomTileMapper @Inject constructor() : QSTileDataToStateMapper<CustomTileData> {
+
+    override fun map(config: QSTileConfig, data: CustomTileData): QSTileState {
+        TODO("Not yet implemented")
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileUserActionInteractor.kt
new file mode 100644
index 0000000..6c1c1a3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileUserActionInteractor.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.qs.tiles.impl.custom
+
+import com.android.systemui.qs.tiles.base.interactor.QSTileInput
+import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.di.QSTileScope
+import javax.inject.Inject
+
+@QSTileScope
+class CustomTileUserActionInteractor @Inject constructor() :
+    QSTileUserActionInteractor<CustomTileData> {
+
+    override suspend fun handleInput(input: QSTileInput<CustomTileData>) {
+        TODO("Not yet implemented")
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileComponent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileComponent.kt
new file mode 100644
index 0000000..01df906
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileComponent.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.qs.tiles.impl.custom.di
+
+import com.android.systemui.qs.tiles.impl.di.QSTileComponent
+import com.android.systemui.qs.tiles.impl.di.QSTileScope
+import dagger.Subcomponent
+
+@QSTileScope
+@Subcomponent(modules = [QSTileConfigModule::class, CustomTileModule::class])
+interface CustomTileComponent : QSTileComponent<Any> {
+
+    @Subcomponent.Builder
+    interface Builder {
+
+        fun qsTileConfigModule(module: QSTileConfigModule): Builder
+
+        fun build(): CustomTileComponent
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileModule.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileModule.kt
new file mode 100644
index 0000000..ccff8af
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileModule.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.tiles.impl.custom.di
+
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.custom.CustomTileData
+import com.android.systemui.qs.tiles.impl.custom.CustomTileInteractor
+import com.android.systemui.qs.tiles.impl.custom.CustomTileMapper
+import com.android.systemui.qs.tiles.impl.custom.CustomTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.custom.di.bound.CustomTileBoundComponent
+import dagger.Binds
+import dagger.Module
+
+/** Provides bindings for QSTile interfaces */
+@Module(subcomponents = [CustomTileBoundComponent::class])
+interface CustomTileModule {
+
+    @Binds
+    fun bindDataInteractor(
+        dataInteractor: CustomTileInteractor
+    ): QSTileDataInteractor<CustomTileData>
+
+    @Binds
+    fun bindUserActionInteractor(
+        userActionInteractor: CustomTileUserActionInteractor
+    ): QSTileUserActionInteractor<CustomTileData>
+
+    @Binds
+    fun bindMapper(customTileMapper: CustomTileMapper): QSTileDataToStateMapper<CustomTileData>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/QSTileConfigModule.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/QSTileConfigModule.kt
new file mode 100644
index 0000000..558fb64
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/QSTileConfigModule.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.tiles.impl.custom.di
+
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import dagger.Module
+import dagger.Provides
+
+/**
+ * Provides [QSTileConfig] and [TileSpec]. To be used along in a QS tile scoped component
+ * implementing [com.android.systemui.qs.tiles.impl.di.QSTileComponent]. In that case it makes it
+ * possible to inject config and tile spec associated with the current tile
+ */
+@Module
+class QSTileConfigModule(private val config: QSTileConfig) {
+
+    @Provides fun provideConfig(): QSTileConfig = config
+
+    @Provides fun provideTileSpec(): TileSpec = config.tileSpec
+
+    @Provides
+    fun provideCustomTileSpec(): TileSpec.CustomTileSpec =
+        config.tileSpec as TileSpec.CustomTileSpec
+
+    @Provides
+    fun providePlatformTileSpec(): TileSpec.PlatformTileSpec =
+        config.tileSpec as TileSpec.PlatformTileSpec
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundComponent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundComponent.kt
new file mode 100644
index 0000000..e33b3e9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundComponent.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.qs.tiles.impl.custom.di.bound
+
+import android.os.UserHandle
+import dagger.BindsInstance
+import dagger.Subcomponent
+import kotlinx.coroutines.CoroutineScope
+
+/** @see CustomTileBoundScope */
+@CustomTileBoundScope
+@Subcomponent
+interface CustomTileBoundComponent {
+
+    @Subcomponent.Builder
+    interface Builder {
+        @BindsInstance fun user(@CustomTileUser user: UserHandle): Builder
+        @BindsInstance fun coroutineScope(@CustomTileBoundScope scope: CoroutineScope): Builder
+
+        fun build(): CustomTileBoundComponent
+    }
+}
diff --git a/core/java/android/hardware/biometrics/IBiometricPromptStatusListener.aidl b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundScope.kt
similarity index 60%
rename from core/java/android/hardware/biometrics/IBiometricPromptStatusListener.aidl
rename to packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundScope.kt
index 7a0f438..4a4ba2b 100644
--- a/core/java/android/hardware/biometrics/IBiometricPromptStatusListener.aidl
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundScope.kt
@@ -14,14 +14,16 @@
  * limitations under the License.
  */
 
-package android.hardware.biometrics;
+package com.android.systemui.qs.tiles.impl.custom.di.bound
+
+import javax.inject.Scope
 
 /**
- * Communication channel to propagate biometric prompt status. Implementation of this interface
- * should be registered in BiometricService#registerBiometricPromptStatusListener.
- * @hide
+ * Scope annotation for bound custom tile scope. This scope lives when a particular
+ * [com.android.systemui.qs.external.CustomTile] is listening and bound to the
+ * [android.service.quicksettings.TileService].
  */
-oneway interface IBiometricPromptStatusListener {
-    void onBiometricPromptShowing();
-    void onBiometricPromptIdle();
-}
\ No newline at end of file
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+@Scope
+annotation class CustomTileBoundScope
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileLifecycle.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileUser.kt
similarity index 71%
copy from packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileLifecycle.kt
copy to packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileUser.kt
index 6d7c576..efc7431 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileLifecycle.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileUser.kt
@@ -14,9 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.systemui.qs.tiles.viewmodel
+package com.android.systemui.qs.tiles.impl.custom.di.bound
 
-enum class QSTileLifecycle {
-    ALIVE,
-    DEAD,
-}
+import javax.inject.Qualifier
+
+/** User associated with current custom tile binding. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class CustomTileUser
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileComponent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileComponent.kt
new file mode 100644
index 0000000..b3d916a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileComponent.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.qs.tiles.impl.di
+
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+
+/**
+ * Base QS tile component. It should be used with [QSTileScope] to create a custom tile scoped
+ * component. Pass this component to
+ * [com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelFactory.Component].
+ */
+interface QSTileComponent<T> {
+
+    fun dataInteractor(): QSTileDataInteractor<T>
+
+    fun userActionInteractor(): QSTileUserActionInteractor<T>
+
+    fun dataToStateMapper(): QSTileDataToStateMapper<T>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileScope.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileScope.kt
new file mode 100644
index 0000000..a412de3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileScope.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.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.tiles.impl.di
+
+import javax.inject.Scope
+
+/**
+ * Scope annotation for QS tiles. This scope is created for each tile and is disposed when the tile
+ * is no longer needed (ex. it's removed from QS). So, it lives along the instance of
+ * [com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelImpl]. This doesn't align with tile
+ * visibility. For example, the tile scope survives shade open/close.
+ */
+@MustBeDocumented @Retention(AnnotationRetention.RUNTIME) @Scope annotation class QSTileScope
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt
index 4a3bcae..c4d7dfb 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt
@@ -16,20 +16,49 @@
 
 package com.android.systemui.qs.tiles.viewmodel
 
+import android.content.res.Resources
+import androidx.annotation.DrawableRes
 import androidx.annotation.StringRes
 import com.android.internal.logging.InstanceId
-import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.qs.pipeline.shared.TileSpec
 
 data class QSTileConfig(
     val tileSpec: TileSpec,
-    val tileIcon: Icon,
-    @StringRes val tileLabelRes: Int,
+    val uiConfig: QSTileUIConfig,
     val instanceId: InstanceId,
     val metricsSpec: String = tileSpec.spec,
     val policy: QSTilePolicy = QSTilePolicy.NoRestrictions,
 )
 
+/**
+ * Static tile icon and label to be used when the fully operational tile isn't needed (ex. in edit
+ * mode). Icon and label are resources to better support config/locale changes.
+ */
+sealed interface QSTileUIConfig {
+
+    val tileIconRes: Int
+        @DrawableRes get
+    val tileLabelRes: Int
+        @StringRes get
+
+    /**
+     * Represents the absence of static UI state. This should be avoided by platform tiles in favour
+     * of [Resource]. Returns [Resources.ID_NULL] for each field.
+     */
+    data object Empty : QSTileUIConfig {
+        override val tileIconRes: Int
+            get() = Resources.ID_NULL
+        override val tileLabelRes: Int
+            get() = Resources.ID_NULL
+    }
+
+    /** Config containing actual icon and label resources. */
+    data class Resource(
+        @StringRes override val tileIconRes: Int,
+        @StringRes override val tileLabelRes: Int,
+    ) : QSTileUIConfig
+}
+
 /** Represents policy restrictions that may be imposed on the tile. */
 sealed interface QSTilePolicy {
     /** Tile has no policy restrictions */
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigProvider.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigProvider.kt
new file mode 100644
index 0000000..3f3b94e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigProvider.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.tiles.viewmodel
+
+import com.android.internal.util.Preconditions
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+
+interface QSTileConfigProvider {
+
+    /**
+     * Returns a [QSTileConfig] for a [tileSpec] or throws [IllegalArgumentException] if there is no
+     * config for such [tileSpec].
+     */
+    fun getConfig(tileSpec: String): QSTileConfig
+}
+
+@SysUISingleton
+class QSTileConfigProviderImpl @Inject constructor(private val configs: Map<String, QSTileConfig>) :
+    QSTileConfigProvider {
+
+    init {
+        for (entry in configs.entries) {
+            val configTileSpec = entry.value.tileSpec.spec
+            val keyTileSpec = entry.key
+            Preconditions.checkArgument(
+                configTileSpec == keyTileSpec,
+                "A wrong config is injected keySpec=$keyTileSpec configSpec=$configTileSpec"
+            )
+        }
+    }
+
+    override fun getConfig(tileSpec: String): QSTileConfig =
+        configs[tileSpec] ?: throw IllegalArgumentException("There is no config for spec=$tileSpec")
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt
index e5cb7ea..580c421 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.qs.tiles.viewmodel
 
+import android.os.UserHandle
 import kotlinx.coroutines.flow.SharedFlow
 import kotlinx.coroutines.flow.StateFlow
 
@@ -29,38 +30,32 @@
  */
 interface QSTileViewModel {
 
-    /**
-     * State of the tile to be shown by the view. It's guaranteed that it's only accessed between
-     * [QSTileLifecycle.ALIVE] and [QSTileLifecycle.DEAD].
-     */
+    /** State of the tile to be shown by the view. */
     val state: SharedFlow<QSTileState>
 
     val config: QSTileConfig
 
-    /**
-     * Specifies whether this device currently supports this tile. This might be called outside of
-     * [QSTileLifecycle.ALIVE] and [QSTileLifecycle.DEAD] bounds (for example in Edit Mode).
-     */
+    /** Specifies whether this device currently supports this tile. */
     val isAvailable: StateFlow<Boolean>
 
     /**
-     * Handles ViewModel lifecycle. Implementations should be inactive outside of
-     * [QSTileLifecycle.ALIVE] and [QSTileLifecycle.DEAD] bounds.
-     */
-    fun onLifecycle(lifecycle: QSTileLifecycle)
-
-    /**
      * Notifies about the user change. Implementations should avoid using 3rd party userId sources
      * and use this value instead. This is to maintain consistent and concurrency-free behaviour
      * across different parts of QS.
      */
-    fun onUserIdChanged(userId: Int)
+    fun onUserChanged(user: UserHandle)
 
     /** Triggers the emission of the new [QSTileState] in a [state]. */
     fun forceUpdate()
 
     /** Notifies underlying logic about user input. */
     fun onActionPerformed(userAction: QSTileUserAction)
+
+    /**
+     * Frees the resources held by this view model. Call it when you no longer need the instance,
+     * because there is no guarantee it will work as expected beyond this point.
+     */
+    fun destroy()
 }
 
 /**
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
index 33f55ab..efa6da7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.qs.tiles.viewmodel
 
 import android.content.Context
+import android.os.UserHandle
 import android.util.Log
 import android.view.View
 import androidx.annotation.GuardedBy
@@ -134,7 +135,7 @@
         qsTileViewModel.currentState?.supportedActions?.contains(action) == true
 
     override fun userSwitch(currentUser: Int) {
-        qsTileViewModel.onUserIdChanged(currentUser)
+        qsTileViewModel.onUserChanged(UserHandle.of(currentUser))
     }
 
     @Deprecated(
@@ -180,7 +181,7 @@
     override fun destroy() {
         stateJob?.cancel()
         availabilityJob?.cancel()
-        qsTileViewModel.onLifecycle(QSTileLifecycle.DEAD)
+        qsTileViewModel.destroy()
     }
 
     override fun getState(): QSTile.State? =
@@ -188,7 +189,13 @@
 
     override fun getInstanceId(): InstanceId = qsTileViewModel.config.instanceId
     override fun getTileLabel(): CharSequence =
-        context.getString(qsTileViewModel.config.tileLabelRes)
+        with(qsTileViewModel.config.uiConfig) {
+            when (this) {
+                is QSTileUIConfig.Empty -> qsTileViewModel.currentState?.label ?: ""
+                is QSTileUIConfig.Resource -> context.getString(tileLabelRes)
+            }
+        }
+
     override fun getTileSpec(): String = qsTileViewModel.config.tileSpec.spec
 
     private companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
index ea1205a..05f125f 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
@@ -163,8 +163,7 @@
         }
 
         mMediaProjectionMetricsLogger.notifyProjectionInitiated(
-                mUserContextProvider.getUserContext().getUserId(),
-                SessionCreationSource.SYSTEM_UI_SCREEN_RECORDER);
+                getHostUid(), SessionCreationSource.SYSTEM_UI_SCREEN_RECORDER);
 
         return flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)
                 ? new ScreenRecordPermissionDialog(
@@ -174,7 +173,8 @@
                         /* controller= */ this,
                         activityStarter,
                         mUserContextProvider,
-                        onStartRecordingClicked)
+                        onStartRecordingClicked,
+                        mMediaProjectionMetricsLogger)
                 : new ScreenRecordDialog(
                         context,
                         /* controller= */ this,
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java
index f2e94e9..e7481cc 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java
@@ -66,8 +66,10 @@
     private Switch mAudioSwitch;
     private Spinner mOptions;
 
-    public ScreenRecordDialog(Context context, RecordingController controller,
-            UserContextProvider userContextProvider, @Nullable Runnable onStartRecordingClicked) {
+    public ScreenRecordDialog(Context context,
+                              RecordingController controller,
+                              UserContextProvider userContextProvider,
+                              @Nullable Runnable onStartRecordingClicked) {
         super(context);
         mController = controller;
         mUserContextProvider = userContextProvider;
@@ -89,7 +91,6 @@
 
         TextView cancelBtn = findViewById(R.id.button_cancel);
         cancelBtn.setOnClickListener(v -> dismiss());
-
         TextView startBtn = findViewById(R.id.button_start);
         startBtn.setOnClickListener(v -> {
             if (mOnStartRecordingClicked != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
index 3b3aa53..f74234b 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
@@ -33,6 +33,7 @@
 import android.widget.Switch
 import androidx.annotation.LayoutRes
 import com.android.systemui.mediaprojection.MediaProjectionCaptureTarget
+import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
 import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorActivity
 import com.android.systemui.mediaprojection.permission.BaseScreenSharePermissionDialog
 import com.android.systemui.mediaprojection.permission.ENTIRE_SCREEN
@@ -50,12 +51,15 @@
     private val controller: RecordingController,
     private val activityStarter: ActivityStarter,
     private val userContextProvider: UserContextProvider,
-    private val onStartRecordingClicked: Runnable?
+    private val onStartRecordingClicked: Runnable?,
+    mediaProjectionMetricsLogger: MediaProjectionMetricsLogger,
 ) :
     BaseScreenSharePermissionDialog(
         context,
         createOptionList(),
         appName = null,
+        hostUid = hostUid,
+        mediaProjectionMetricsLogger,
         R.drawable.ic_screenrecord,
         R.color.screenrecord_icon_color
     ) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 8dc97c0..cc59f87 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -2629,7 +2629,6 @@
         if (isPanelExpanded() != isExpanded) {
             setExpandedOrAwaitingInputTransfer(isExpanded);
             updateSystemUiStateFlags();
-            mShadeExpansionStateManager.onShadeExpansionFullyChanged(isExpanded);
             if (!isExpanded) {
                 mQsController.closeQsCustomizer();
             }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
index 3c68438..0ec7a36 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
@@ -65,7 +65,6 @@
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.DejankUtils;
 import com.android.systemui.Dumpable;
-import com.android.systemui.res.R;
 import com.android.systemui.classifier.Classifier;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dump.DumpManager;
@@ -77,6 +76,7 @@
 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.res.R;
 import com.android.systemui.screenrecord.RecordingController;
 import com.android.systemui.shade.data.repository.ShadeRepository;
 import com.android.systemui.shade.domain.interactor.ShadeInteractor;
@@ -1026,7 +1026,6 @@
                 && mPanelViewControllerLazy.get().mAnimateBack) {
             mPanelViewControllerLazy.get().adjustBackAnimationScale(adjustedExpansionFraction);
         }
-        mShadeExpansionStateManager.onQsExpansionFractionChanged(qsExpansionFraction);
         mMediaHierarchyManager.setQsExpansion(qsExpansionFraction);
         int qsPanelBottomY = calculateBottomPosition(qsExpansionFraction);
         mScrimController.setQsPosition(qsExpansionFraction, qsPanelBottomY);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
index 0554c58..9493989 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
@@ -36,10 +36,7 @@
 class ShadeExpansionStateManager @Inject constructor() : ShadeStateEvents {
 
     private val expansionListeners = CopyOnWriteArrayList<ShadeExpansionListener>()
-    private val fullExpansionListeners = CopyOnWriteArrayList<ShadeFullExpansionListener>()
     private val qsExpansionListeners = CopyOnWriteArrayList<ShadeQsExpansionListener>()
-    private val qsExpansionFractionListeners =
-        CopyOnWriteArrayList<ShadeQsExpansionFractionListener>()
     private val stateListeners = CopyOnWriteArrayList<ShadeStateListener>()
     private val shadeStateEventsListeners = CopyOnWriteArrayList<ShadeStateEventsListener>()
 
@@ -67,15 +64,6 @@
         expansionListeners.remove(listener)
     }
 
-    fun addFullExpansionListener(listener: ShadeFullExpansionListener) {
-        fullExpansionListeners.add(listener)
-        listener.onShadeExpansionFullyChanged(qsExpanded)
-    }
-
-    fun removeFullExpansionListener(listener: ShadeFullExpansionListener) {
-        fullExpansionListeners.remove(listener)
-    }
-
     fun addQsExpansionListener(listener: ShadeQsExpansionListener) {
         qsExpansionListeners.add(listener)
         listener.onQsExpansionChanged(qsExpanded)
@@ -85,25 +73,11 @@
         qsExpansionListeners.remove(listener)
     }
 
-    fun addQsExpansionFractionListener(listener: ShadeQsExpansionFractionListener) {
-        qsExpansionFractionListeners.add(listener)
-        listener.onQsExpansionFractionChanged(qsExpansionFraction)
-    }
-
-    fun removeQsExpansionFractionListener(listener: ShadeQsExpansionFractionListener) {
-        qsExpansionFractionListeners.remove(listener)
-    }
-
     /** Adds a listener that will be notified when the panel state has changed. */
     fun addStateListener(listener: ShadeStateListener) {
         stateListeners.add(listener)
     }
 
-    /** Removes a state listener. */
-    fun removeStateListener(listener: ShadeStateListener) {
-        stateListeners.remove(listener)
-    }
-
     override fun addShadeStateEventsListener(listener: ShadeStateEventsListener) {
         shadeStateEventsListeners.addIfAbsent(listener)
     }
@@ -187,22 +161,6 @@
         qsExpansionListeners.forEach { it.onQsExpansionChanged(qsExpanded) }
     }
 
-    fun onQsExpansionFractionChanged(qsExpansionFraction: Float) {
-        this.qsExpansionFraction = qsExpansionFraction
-
-        debugLog("qsExpansionFraction=$qsExpansionFraction")
-        qsExpansionFractionListeners.forEach {
-            it.onQsExpansionFractionChanged(qsExpansionFraction)
-        }
-    }
-
-    fun onShadeExpansionFullyChanged(isExpanded: Boolean) {
-        this.expanded = isExpanded
-
-        debugLog("expanded=$isExpanded")
-        fullExpansionListeners.forEach { it.onShadeExpansionFullyChanged(isExpanded) }
-    }
-
     /** Updates the panel state if necessary. */
     fun updateState(@PanelState state: Int) {
         debugLog(
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeFullExpansionListener.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeFullExpansionListener.kt
deleted file mode 100644
index 6d13e19..0000000
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeFullExpansionListener.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (c) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.shade
-
-/** A listener interface to be notified of expansion events for the notification shade. */
-fun interface ShadeFullExpansionListener {
-    /** Invoked whenever the shade expansion changes, when it is fully collapsed or expanded */
-    fun onShadeExpansionFullyChanged(isExpanded: Boolean)
-}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
index 2f68476..f043c71 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
@@ -148,12 +148,13 @@
      *
      * TODO(b/300258424) remove all but the first sentence of this comment
      */
-    val isAnyExpanded: Flow<Boolean> =
+    val isAnyExpanded: StateFlow<Boolean> =
         if (sceneContainerFlags.isEnabled()) {
-            anyExpansion.map { it > 0f }.distinctUntilChanged()
-        } else {
-            repository.legacyExpandedOrAwaitingInputTransfer
-        }
+                anyExpansion.map { it > 0f }.distinctUntilChanged()
+            } else {
+                repository.legacyExpandedOrAwaitingInputTransfer
+            }
+            .stateIn(scope, SharingStarted.Eagerly, false)
 
     /**
      * Whether the user is expanding or collapsing the shade with user input. This will be true even
@@ -184,7 +185,7 @@
      * but a transition they initiated is still animating.
      */
     val isUserInteracting: Flow<Boolean> =
-        combine(isUserInteractingWithShade, isUserInteractingWithShade) { shade, qs -> shade || qs }
+        combine(isUserInteractingWithShade, isUserInteractingWithQs) { shade, qs -> shade || qs }
             .distinctUntilChanged()
 
     /** Are touches allowed on the notification panel? */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java
index 7d81e55..c8669072 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java
@@ -48,6 +48,13 @@
         fadeOut(view, duration, delay, (Runnable) null);
     }
 
+    /**
+     * Perform a fade-out animation, invoking {@code endRunnable} when the animation ends. It will
+     * not be invoked if the animation is cancelled.
+     *
+     * @deprecated Use {@link #fadeOut(View, long, int, Animator.AnimatorListener)} instead.
+     */
+    @Deprecated
     public static void fadeOut(final View view, long duration, int delay,
             @Nullable final Runnable endRunnable) {
         view.animate().cancel();
@@ -155,6 +162,13 @@
         fadeIn(view, duration, delay, /* endRunnable= */ (Runnable) null);
     }
 
+    /**
+     * Perform a fade-in animation, invoking {@code endRunnable} when the animation ends. It will
+     * not be invoked if the animation is cancelled.
+     *
+     * @deprecated Use {@link #fadeIn(View, long, int, Animator.AnimatorListener)} instead.
+     */
+    @Deprecated
     public static void fadeIn(final View view, long duration, int delay,
             @Nullable Runnable endRunnable) {
         view.animate().cancel();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
index 2e1e395..49743bf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
@@ -29,14 +29,13 @@
 
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.flags.FeatureFlagsClassic;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.PluginManager;
 import com.android.systemui.statusbar.dagger.CentralSurfacesModule;
 import com.android.systemui.statusbar.domain.interactor.SilentNotificationStatusIconsVisibilityInteractor;
 import com.android.systemui.statusbar.notification.collection.NotifCollection;
 import com.android.systemui.statusbar.notification.collection.PipelineDumpable;
 import com.android.systemui.statusbar.notification.collection.PipelineDumper;
+import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.NotificationListenerWithPlugins;
 import com.android.systemui.util.time.SystemClock;
@@ -62,7 +61,6 @@
     private static final long MAX_RANKING_DELAY_MILLIS = 500L;
 
     private final Context mContext;
-    private final FeatureFlagsClassic mFeatureFlags;
     private final NotificationManager mNotificationManager;
     private final SilentNotificationStatusIconsVisibilityInteractor mStatusIconInteractor;
     private final SystemClock mSystemClock;
@@ -80,7 +78,6 @@
     @Inject
     public NotificationListener(
             Context context,
-            FeatureFlagsClassic featureFlags,
             NotificationManager notificationManager,
             SilentNotificationStatusIconsVisibilityInteractor statusIconInteractor,
             SystemClock systemClock,
@@ -88,7 +85,6 @@
             PluginManager pluginManager) {
         super(pluginManager);
         mContext = context;
-        mFeatureFlags = featureFlags;
         mNotificationManager = notificationManager;
         mStatusIconInteractor = statusIconInteractor;
         mSystemClock = systemClock;
@@ -106,6 +102,7 @@
     /** Registers a listener that's notified when any notification-related settings change. */
     @Deprecated
     public void addNotificationSettingsListener(NotificationSettingsListener listener) {
+        NotificationIconContainerRefactor.assertInLegacyMode();
         mSettingsListeners.add(listener);
     }
 
@@ -240,7 +237,7 @@
 
     @Override
     public void onSilentStatusBarIconsVisibilityChanged(boolean hideSilentStatusIcons) {
-        if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR)) {
+        if (NotificationIconContainerRefactor.isEnabled()) {
             mStatusIconInteractor.setHideSilentStatusIcons(hideSilentStatusIcons);
         } else {
             for (NotificationSettingsListener listener : mSettingsListeners) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
index 61ebcc0..5f0b298 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
@@ -48,11 +48,13 @@
 
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.internal.statusbar.NotificationVisibility;
+import com.android.systemui.CoreStartable;
 import com.android.systemui.Dumpable;
-import com.android.systemui.dump.DumpManager;
+import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.power.domain.interactor.PowerInteractor;
 import com.android.systemui.res.R;
+import com.android.systemui.shade.domain.interactor.ShadeInteractor;
 import com.android.systemui.statusbar.dagger.CentralSurfacesDependenciesModule;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.RemoteInputControllerLogger;
@@ -65,6 +67,7 @@
 import com.android.systemui.statusbar.policy.RemoteInputView;
 import com.android.systemui.util.DumpUtilsKt;
 import com.android.systemui.util.ListenerSet;
+import com.android.systemui.util.kotlin.JavaAdapter;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -72,13 +75,16 @@
 import java.util.Objects;
 import java.util.function.Consumer;
 
+import javax.inject.Inject;
+
 /**
  * Class for handling remote input state over a set of notifications. This class handles things
  * like keeping notifications temporarily that were cancelled as a response to a remote input
  * interaction, keeping track of notifications to remove when NotificationPresenter is collapsed,
  * and handling clicks on remote views.
  */
-public class NotificationRemoteInputManager implements Dumpable {
+@SysUISingleton
+public class NotificationRemoteInputManager implements CoreStartable {
     public static final boolean ENABLE_REMOTE_INPUT =
             SystemProperties.getBoolean("debug.enable_remote_input", true);
     public static boolean FORCE_REMOTE_INPUT_HISTORY =
@@ -94,6 +100,8 @@
     private final NotificationVisibilityProvider mVisibilityProvider;
     private final PowerInteractor mPowerInteractor;
     private final ActionClickLogger mLogger;
+    private final JavaAdapter mJavaAdapter;
+    private final ShadeInteractor mShadeInteractor;
     protected final Context mContext;
     protected final NotifPipelineFlags mNotifPipelineFlags;
     private final UserManager mUserManager;
@@ -249,6 +257,7 @@
     /**
      * Injected constructor. See {@link CentralSurfacesDependenciesModule}.
      */
+    @Inject
     public NotificationRemoteInputManager(
             Context context,
             NotifPipelineFlags notifPipelineFlags,
@@ -261,7 +270,8 @@
             RemoteInputControllerLogger remoteInputControllerLogger,
             NotificationClickNotifier clickNotifier,
             ActionClickLogger logger,
-            DumpManager dumpManager) {
+            JavaAdapter javaAdapter,
+            ShadeInteractor shadeInteractor) {
         mContext = context;
         mNotifPipelineFlags = notifPipelineFlags;
         mLockscreenUserManager = lockscreenUserManager;
@@ -269,6 +279,8 @@
         mVisibilityProvider = visibilityProvider;
         mPowerInteractor = powerInteractor;
         mLogger = logger;
+        mJavaAdapter = javaAdapter;
+        mShadeInteractor = shadeInteractor;
         mBarService = IStatusBarService.Stub.asInterface(
                 ServiceManager.getService(Context.STATUS_BAR_SERVICE));
         mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
@@ -277,8 +289,25 @@
         mRemoteInputUriController = remoteInputUriController;
         mRemoteInputControllerLogger = remoteInputControllerLogger;
         mClickNotifier = clickNotifier;
+    }
 
-        dumpManager.registerDumpable(this);
+    @Override
+    public void start() {
+        mJavaAdapter.alwaysCollectFlow(mShadeInteractor.isAnyExpanded(),
+                this::onShadeOrQsExpanded);
+    }
+
+    private void onShadeOrQsExpanded(boolean expanded) {
+        if (expanded && mStatusBarStateController.getState() != StatusBarState.KEYGUARD) {
+            try {
+                mBarService.clearNotificationEffects();
+            } catch (RemoteException e) {
+                // Won't fail unless the world has ended.
+            }
+        }
+        if (!expanded) {
+            onPanelCollapsed();
+        }
     }
 
     /** Add a listener for various remote input events.  Works with NEW pipeline only. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
index 37a4ef1..537f8a8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
@@ -42,15 +42,16 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.keyguard.KeyguardClockSwitch;
 import com.android.systemui.DejankUtils;
-import com.android.systemui.Dumpable;
-import com.android.systemui.res.R;
 import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
-import com.android.systemui.shade.ShadeExpansionStateManager;
+import com.android.systemui.res.R;
+import com.android.systemui.shade.domain.interactor.ShadeInteractor;
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 import com.android.systemui.statusbar.policy.CallbackController;
 import com.android.systemui.util.Compile;
+import com.android.systemui.util.kotlin.JavaAdapter;
+
+import dagger.Lazy;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -64,8 +65,7 @@
 @SysUISingleton
 public class StatusBarStateControllerImpl implements
         SysuiStatusBarStateController,
-        CallbackController<StateListener>,
-        Dumpable {
+        CallbackController<StateListener> {
     private static final String TAG = "SbStateController";
     private static final boolean DEBUG_IMMERSIVE_APPS =
             SystemProperties.getBoolean("persist.debug.immersive_apps", false);
@@ -95,6 +95,8 @@
     private final ArrayList<RankedListener> mListeners = new ArrayList<>();
     private final UiEventLogger mUiEventLogger;
     private final InteractionJankMonitor mInteractionJankMonitor;
+    private final JavaAdapter mJavaAdapter;
+    private final Lazy<ShadeInteractor> mShadeInteractorLazy;
     private int mState;
     private int mLastState;
     private int mUpcomingState;
@@ -156,18 +158,22 @@
     @Inject
     public StatusBarStateControllerImpl(
             UiEventLogger uiEventLogger,
-            DumpManager dumpManager,
             InteractionJankMonitor interactionJankMonitor,
-            ShadeExpansionStateManager shadeExpansionStateManager
-    ) {
+            JavaAdapter javaAdapter,
+            Lazy<ShadeInteractor> shadeInteractorLazy) {
         mUiEventLogger = uiEventLogger;
         mInteractionJankMonitor = interactionJankMonitor;
+        mJavaAdapter = javaAdapter;
+        mShadeInteractorLazy = shadeInteractorLazy;
         for (int i = 0; i < HISTORY_SIZE; i++) {
             mHistoricalRecords[i] = new HistoricalState();
         }
-        shadeExpansionStateManager.addFullExpansionListener(this::onShadeExpansionFullyChanged);
+    }
 
-        dumpManager.registerDumpable(this);
+    @Override
+    public void start() {
+        mJavaAdapter.alwaysCollectFlow(mShadeInteractorLazy.get().isAnyExpanded(),
+                this::onShadeOrQsExpanded);
     }
 
     @Override
@@ -345,7 +351,7 @@
         }
     }
 
-    private void onShadeExpansionFullyChanged(Boolean isExpanded) {
+    private void onShadeOrQsExpanded(Boolean isExpanded) {
         if (mIsExpanded != isExpanded) {
             mIsExpanded = isExpanded;
             String tag = getClass().getSimpleName() + "#setIsExpanded";
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java b/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java
index aa32d5c..8104755 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java
@@ -21,6 +21,7 @@
 import android.annotation.IntDef;
 import android.view.View;
 
+import com.android.systemui.CoreStartable;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 
@@ -29,7 +30,7 @@
 /**
  * Sends updates to {@link StateListener}s about changes to the status bar state and dozing state
  */
-public interface SysuiStatusBarStateController extends StatusBarStateController {
+public interface SysuiStatusBarStateController extends StatusBarStateController, CoreStartable {
 
     // TODO: b/115739177 (remove this explicit ordering if we can)
     @Retention(SOURCE)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
index 1fe6b83..a957095 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
@@ -23,6 +23,7 @@
 
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.statusbar.IStatusBarService;
+import com.android.systemui.CoreStartable;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.animation.AnimationFeatureFlags;
 import com.android.systemui.animation.DialogLaunchAnimator;
@@ -33,24 +34,19 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.media.controls.pipeline.MediaDataManager;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.power.domain.interactor.PowerInteractor;
 import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.ShadeSurface;
 import com.android.systemui.shade.carrier.ShadeCarrierGroupController;
-import com.android.systemui.statusbar.ActionClickLogger;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationClickNotifier;
-import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.SmartReplyController;
 import com.android.systemui.statusbar.StatusBarStateControllerImpl;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.commandline.CommandRegistry;
-import com.android.systemui.statusbar.notification.NotifPipelineFlags;
-import com.android.systemui.statusbar.notification.RemoteInputControllerLogger;
 import com.android.systemui.statusbar.notification.collection.NotifCollection;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
@@ -63,12 +59,13 @@
 import com.android.systemui.statusbar.phone.StatusBarNotificationPresenterModule;
 import com.android.systemui.statusbar.phone.StatusBarRemoteInputCallback;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.statusbar.policy.RemoteInputUriController;
 
 import dagger.Binds;
 import dagger.Lazy;
 import dagger.Module;
 import dagger.Provides;
+import dagger.multibindings.ClassKey;
+import dagger.multibindings.IntoMap;
 
 /**
  * This module provides instances needed to construct {@link CentralSurfacesImpl}. These are moved to
@@ -78,36 +75,12 @@
  */
 @Module(includes = {StatusBarNotificationPresenterModule.class})
 public interface CentralSurfacesDependenciesModule {
+
     /** */
-    @SysUISingleton
-    @Provides
-    static NotificationRemoteInputManager provideNotificationRemoteInputManager(
-            Context context,
-            NotifPipelineFlags notifPipelineFlags,
-            NotificationLockscreenUserManager lockscreenUserManager,
-            SmartReplyController smartReplyController,
-            NotificationVisibilityProvider visibilityProvider,
-            PowerInteractor powerInteractor,
-            StatusBarStateController statusBarStateController,
-            RemoteInputUriController remoteInputUriController,
-            RemoteInputControllerLogger remoteInputControllerLogger,
-            NotificationClickNotifier clickNotifier,
-            ActionClickLogger actionClickLogger,
-            DumpManager dumpManager) {
-        return new NotificationRemoteInputManager(
-                context,
-                notifPipelineFlags,
-                lockscreenUserManager,
-                smartReplyController,
-                visibilityProvider,
-                powerInteractor,
-                statusBarStateController,
-                remoteInputUriController,
-                remoteInputControllerLogger,
-                clickNotifier,
-                actionClickLogger,
-                dumpManager);
-    }
+    @Binds
+    @IntoMap
+    @ClassKey(NotificationRemoteInputManager.class)
+    CoreStartable bindsStartNotificationRemoteInputManager(NotificationRemoteInputManager nrim);
 
     /** */
     @SysUISingleton
@@ -164,20 +137,23 @@
         return new CommandQueue(context, displayTracker, registry, dumpHandler, powerInteractor);
     }
 
-    /**
-     */
+    /** */
     @Binds
     ManagedProfileController provideManagedProfileController(
             ManagedProfileControllerImpl controllerImpl);
 
-    /**
-     */
+    /** */
     @Binds
     SysuiStatusBarStateController providesSysuiStatusBarStateController(
             StatusBarStateControllerImpl statusBarStateControllerImpl);
 
-    /**
-     */
+    /** */
+    @Binds
+    @IntoMap
+    @ClassKey(SysuiStatusBarStateController.class)
+    CoreStartable bindsStartStatusBarStateController(StatusBarStateControllerImpl sbsc);
+
+    /** */
     @Binds
     StatusBarIconController provideStatusBarIconController(
             StatusBarIconControllerImpl controllerImpl);
@@ -212,16 +188,14 @@
     ShadeCarrierGroupController.SlotIndexResolver provideSlotIndexResolver(
             ShadeCarrierGroupController.SubscriptionManagerSlotIndexResolver impl);
 
-    /**
-     */
+    /** */
     @Provides
     @SysUISingleton
     static ActivityLaunchAnimator provideActivityLaunchAnimator() {
         return new ActivityLaunchAnimator();
     }
 
-    /**
-     */
+    /** */
     @Provides
     @SysUISingleton
     static DialogLaunchAnimator provideDialogLaunchAnimator(IDreamManager dreamManager,
@@ -253,8 +227,7 @@
         return new DialogLaunchAnimator(callback, interactionJankMonitor, animationFeatureFlags);
     }
 
-    /**
-     */
+    /** */
     @Provides
     @SysUISingleton
     static AnimationFeatureFlags provideAnimationFeatureFlags(FeatureFlags featureFlags) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
index 07e84bb..e0c4bfa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
@@ -16,8 +16,6 @@
 
 package com.android.systemui.statusbar.notification.collection.coordinator
 
-import com.android.systemui.flags.FeatureFlagsClassic
-import com.android.systemui.flags.Flags
 import com.android.systemui.statusbar.notification.collection.ListEntry
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
 import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
@@ -25,6 +23,7 @@
 import com.android.systemui.statusbar.notification.collection.render.NotifStackController
 import com.android.systemui.statusbar.notification.collection.render.NotifStats
 import com.android.systemui.statusbar.notification.domain.interactor.RenderNotificationListInteractor
+import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
 import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT
 import com.android.systemui.statusbar.phone.NotificationIconAreaController
 import com.android.systemui.util.traceSection
@@ -38,7 +37,6 @@
 class StackCoordinator
 @Inject
 internal constructor(
-    private val featureFlags: FeatureFlagsClassic,
     private val groupExpansionManagerImpl: GroupExpansionManagerImpl,
     private val notificationIconAreaController: NotificationIconAreaController,
     private val renderListInteractor: RenderNotificationListInteractor,
@@ -52,7 +50,7 @@
     fun onAfterRenderList(entries: List<ListEntry>, controller: NotifStackController) =
         traceSection("StackCoordinator.onAfterRenderList") {
             controller.setNotifStats(calculateNotifStats(entries))
-            if (featureFlags.isEnabled(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR)) {
+            if (NotificationIconContainerRefactor.isEnabled) {
                 renderListInteractor.setRenderedList(entries)
             } else {
                 notificationIconAreaController.updateNotificationIcons(entries)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt
index 246933a..1992ea8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt
@@ -18,24 +18,15 @@
 import android.content.Context
 import android.graphics.Rect
 import android.view.View
-import com.android.systemui.common.ui.ConfigurationState
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.flags.FeatureFlagsClassic
-import com.android.systemui.flags.Flags
-import com.android.systemui.flags.RefactorFlag
 import com.android.systemui.statusbar.NotificationShelfController
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.notification.collection.ListEntry
-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.shared.NotificationIconContainerRefactor
 import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinderWrapperControllerImpl
-import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.phone.NotificationIconAreaController
 import com.android.systemui.statusbar.phone.NotificationIconContainer
-import com.android.systemui.statusbar.phone.ScreenOffAnimationController
-import com.android.systemui.statusbar.policy.ConfigurationController
 import javax.inject.Inject
-import kotlinx.coroutines.DisposableHandle
 
 /**
  * Controller class for [NotificationIconContainer]. This implementation serves as a temporary
@@ -45,67 +36,16 @@
  * can be used directly.
  */
 @SysUISingleton
-class NotificationIconAreaControllerViewBinderWrapperImpl
-@Inject
-constructor(
-    private val configuration: ConfigurationState,
-    private val configurationController: ConfigurationController,
-    private val dozeParameters: DozeParameters,
-    private val featureFlags: FeatureFlagsClassic,
-    private val screenOffAnimationController: ScreenOffAnimationController,
-    private val shelfIconViewStore: ShelfNotificationIconViewStore,
-    private val shelfIconsViewModel: NotificationIconContainerShelfViewModel,
-    private val aodIconViewStore: AlwaysOnDisplayNotificationIconViewStore,
-    private val aodIconsViewModel: NotificationIconContainerAlwaysOnDisplayViewModel,
-) : NotificationIconAreaController {
-
-    private val shelfRefactor = RefactorFlag(featureFlags, Flags.NOTIFICATION_SHELF_REFACTOR)
-
-    private var shelfIcons: NotificationIconContainer? = null
-    private var aodIcons: NotificationIconContainer? = null
-    private var aodBindJob: DisposableHandle? = null
+class NotificationIconAreaControllerViewBinderWrapperImpl @Inject constructor() :
+    NotificationIconAreaController {
 
     /** Called by the Keyguard*ViewController whose view contains the aod icons. */
-    override fun setupAodIcons(aodIcons: NotificationIconContainer) {
-        val changed = this.aodIcons != null && aodIcons !== this.aodIcons
-        if (changed) {
-            this.aodIcons!!.setAnimationsEnabled(false)
-            this.aodIcons!!.removeAllViews()
-            aodBindJob?.dispose()
-        }
-        this.aodIcons = aodIcons
-        this.aodIcons!!.setOnLockScreen(true)
-        aodBindJob =
-            NotificationIconContainerViewBinder.bind(
-                aodIcons,
-                aodIconsViewModel,
-                configuration,
-                configurationController,
-                dozeParameters,
-                featureFlags,
-                screenOffAnimationController,
-                aodIconViewStore,
-            )
-    }
+    override fun setupAodIcons(aodIcons: NotificationIconContainer?) = unsupported
 
     override fun setupShelf(notificationShelfController: NotificationShelfController) =
         NotificationShelfViewBinderWrapperControllerImpl.unsupported
 
-    override fun setShelfIcons(icons: NotificationIconContainer) {
-        if (shelfRefactor.isUnexpectedlyInLegacyMode()) {
-            NotificationIconContainerViewBinder.bind(
-                icons,
-                shelfIconsViewModel,
-                configuration,
-                configurationController,
-                dozeParameters,
-                featureFlags,
-                screenOffAnimationController,
-                shelfIconViewStore,
-            )
-            shelfIcons = icons
-        }
-    }
+    override fun setShelfIcons(icons: NotificationIconContainer) = unsupported
 
     override fun onDensityOrFontScaleChanged(context: Context) = unsupported
 
@@ -126,15 +66,14 @@
 
     override fun onThemeChanged() = unsupported
 
-    override fun getHeight(): Int {
-        return if (aodIcons == null) 0 else aodIcons!!.height
-    }
+    override fun getHeight(): Int = unsupported
 
     companion object {
         val unsupported: Nothing
             get() =
                 error(
-                    "Code path not supported when NOTIFICATION_ICON_CONTAINER_REFACTOR is disabled"
+                    "Code path not supported when ${NotificationIconContainerRefactor.FLAG_NAME}" +
+                        " is disabled"
                 )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
index 7592619..c1f728a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
@@ -17,13 +17,15 @@
 
 import android.animation.Animator
 import android.animation.AnimatorListenerAdapter
+import android.graphics.Color
 import android.graphics.Rect
 import android.view.View
+import android.view.ViewGroup
 import android.view.ViewPropertyAnimator
 import android.widget.FrameLayout
+import androidx.annotation.ColorInt
 import androidx.collection.ArrayMap
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
+import androidx.lifecycle.lifecycleScope
 import com.android.app.animation.Interpolators
 import com.android.internal.policy.SystemBarUtils
 import com.android.internal.util.ContrastColorUtil
@@ -37,9 +39,12 @@
 import com.android.systemui.statusbar.notification.NotificationUtils
 import com.android.systemui.statusbar.notification.collection.NotifCollection
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.IconViewStore
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.IconColors
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.IconsViewData
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconColorLookup
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconColors
+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.statusbar.notification.icon.ui.viewmodel.NotificationIconsViewData
 import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.phone.NotificationIconContainer
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
@@ -49,6 +54,7 @@
 import com.android.systemui.util.kotlin.mapValuesNotNullTo
 import com.android.systemui.util.kotlin.sample
 import com.android.systemui.util.kotlin.stateFlow
+import com.android.systemui.util.ui.AnimatedValue
 import com.android.systemui.util.ui.isAnimating
 import com.android.systemui.util.ui.stopAnimating
 import com.android.systemui.util.ui.value
@@ -62,12 +68,53 @@
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 
-/** Binds a [NotificationIconContainer] to its [view model][NotificationIconContainerViewModel]. */
+/** Binds a view-model to a [NotificationIconContainer]. */
 object NotificationIconContainerViewBinder {
     @JvmStatic
     fun bind(
         view: NotificationIconContainer,
-        viewModel: NotificationIconContainerViewModel,
+        viewModel: NotificationIconContainerShelfViewModel,
+        configuration: ConfigurationState,
+        configurationController: ConfigurationController,
+        viewStore: ShelfNotificationIconViewStore,
+    ): DisposableHandle {
+        return view.repeatWhenAttached {
+            lifecycleScope.launch {
+                viewModel.icons.bindIcons(view, configuration, configurationController, viewStore)
+            }
+        }
+    }
+
+    @JvmStatic
+    fun bind(
+        view: NotificationIconContainer,
+        viewModel: NotificationIconContainerStatusBarViewModel,
+        configuration: ConfigurationState,
+        configurationController: ConfigurationController,
+        viewStore: StatusBarNotificationIconViewStore,
+    ): DisposableHandle {
+        val contrastColorUtil = ContrastColorUtil.getInstance(view.context)
+        return view.repeatWhenAttached {
+            lifecycleScope.run {
+                launch {
+                    viewModel.icons.bindIcons(
+                        view,
+                        configuration,
+                        configurationController,
+                        viewStore
+                    )
+                }
+                launch { viewModel.iconColors.bindIconColors(view, contrastColorUtil) }
+                launch { viewModel.bindIsolatedIcon(view, viewStore) }
+                launch { viewModel.animationsEnabled.bindAnimationsEnabled(view) }
+            }
+        }
+    }
+
+    @JvmStatic
+    fun bind(
+        view: NotificationIconContainer,
+        viewModel: NotificationIconContainerAlwaysOnDisplayViewModel,
         configuration: ConfigurationState,
         configurationController: ConfigurationController,
         dozeParameters: DozeParameters,
@@ -75,60 +122,72 @@
         screenOffAnimationController: ScreenOffAnimationController,
         viewStore: IconViewStore,
     ): DisposableHandle {
-        val contrastColorUtil = ContrastColorUtil.getInstance(view.context)
         return view.repeatWhenAttached {
-            repeatOnLifecycle(Lifecycle.State.CREATED) {
-                launch { bindAnimationsEnabled(viewModel, view) }
-                launch { bindIsDozing(viewModel, view, dozeParameters) }
-                // TODO(b/278765923): this should live where AOD is bound, not inside of the NIC
-                //  view-binder
+            lifecycleScope.launch {
                 launch {
-                    bindVisibility(
-                        viewModel,
-                        view,
-                        configuration,
-                        featureFlags,
-                        screenOffAnimationController,
-                    )
-                }
-                launch { bindIconColors(viewModel, view, contrastColorUtil) }
-                launch {
-                    bindIconViewData(
-                        viewModel,
+                    viewModel.icons.bindIcons(
                         view,
                         configuration,
                         configurationController,
                         viewStore,
                     )
                 }
-                launch { bindIsolatedIcon(viewModel, view, viewStore) }
+                launch { viewModel.animationsEnabled.bindAnimationsEnabled(view) }
+                launch { viewModel.isDozing.bindIsDozing(view, dozeParameters) }
+                // TODO(b/278765923): this should live where AOD is bound, not inside of the NIC
+                //  view-binder
+                launch {
+                    viewModel.isVisible.bindIsVisible(
+                        view,
+                        configuration,
+                        featureFlags,
+                        screenOffAnimationController,
+                    )
+                }
+                launch {
+                    configuration
+                        .getColorAttr(R.attr.wallpaperTextColor, DEFAULT_AOD_ICON_COLOR)
+                        .bindIconColors(view)
+                }
             }
         }
     }
 
-    private suspend fun bindAnimationsEnabled(
-        viewModel: NotificationIconContainerViewModel,
-        view: NotificationIconContainer
-    ) {
-        viewModel.animationsEnabled.collect(view::setAnimationsEnabled)
+    /** Binds to [NotificationIconContainer.setAnimationsEnabled] */
+    private suspend fun Flow<Boolean>.bindAnimationsEnabled(view: NotificationIconContainer) {
+        collect(view::setAnimationsEnabled)
     }
 
-    private suspend fun bindIconColors(
-        viewModel: NotificationIconContainerViewModel,
+    /**
+     * Binds to the [StatusBarIconView.setStaticDrawableColor] and [StatusBarIconView.setDecorColor]
+     * of the [children] of an [NotificationIconContainer].
+     */
+    private suspend fun Flow<NotificationIconColorLookup>.bindIconColors(
         view: NotificationIconContainer,
         contrastColorUtil: ContrastColorUtil,
     ) {
-        viewModel.iconColors
-            .mapNotNull { lookup -> lookup.iconColors(view.viewBounds) }
-            .collect { iconLookup -> applyTint(view, iconLookup, contrastColorUtil) }
+        mapNotNull { lookup -> lookup.iconColors(view.viewBounds) }
+            .collect { iconLookup -> view.applyTint(iconLookup, contrastColorUtil) }
     }
 
-    private suspend fun bindIsDozing(
-        viewModel: NotificationIconContainerViewModel,
+    /**
+     * Binds to the [StatusBarIconView.setStaticDrawableColor] and [StatusBarIconView.setDecorColor]
+     * of the [children] of an [NotificationIconContainer].
+     */
+    private suspend fun Flow<Int>.bindIconColors(view: NotificationIconContainer) {
+        collect { tint ->
+            view.children.filterIsInstance<StatusBarIconView>().forEach { icon ->
+                icon.staticDrawableColor = tint
+                icon.setDecorColor(tint)
+            }
+        }
+    }
+
+    private suspend fun Flow<AnimatedValue<Boolean>>.bindIsDozing(
         view: NotificationIconContainer,
         dozeParameters: DozeParameters,
     ) {
-        viewModel.isDozing.collect { isDozing ->
+        collect { isDozing ->
             if (isDozing.isAnimating) {
                 val animate = !dozeParameters.displayNeedsBlanking
                 view.setDozing(
@@ -147,19 +206,18 @@
         }
     }
 
-    private suspend fun bindIsolatedIcon(
-        viewModel: NotificationIconContainerViewModel,
+    private suspend fun NotificationIconContainerStatusBarViewModel.bindIsolatedIcon(
         view: NotificationIconContainer,
         viewStore: IconViewStore,
     ) {
         coroutineScope {
             launch {
-                viewModel.isolatedIconLocation.collect { location ->
+                isolatedIconLocation.collect { location ->
                     view.setIsolatedIconLocation(location, true)
                 }
             }
             launch {
-                viewModel.isolatedIcon.collect { iconInfo ->
+                isolatedIcon.collect { iconInfo ->
                     val iconView = iconInfo.value?.let { viewStore.iconView(it.notifKey) }
                     if (iconInfo.isAnimating) {
                         view.showIconIsolatedAnimated(iconView, iconInfo::stopAnimating)
@@ -171,8 +229,8 @@
         }
     }
 
-    private suspend fun bindIconViewData(
-        viewModel: NotificationIconContainerViewModel,
+    /** Binds [NotificationIconsViewData] to a [NotificationIconContainer]'s [children]. */
+    private suspend fun Flow<NotificationIconsViewData>.bindIcons(
         view: NotificationIconContainer,
         configuration: ConfigurationState,
         configurationController: ConfigurationController,
@@ -205,11 +263,11 @@
             }
         }
 
-        var prevIcons = IconsViewData()
-        viewModel.iconsViewData.sample(layoutParams, ::Pair).collect {
-            (iconsData: IconsViewData, layoutParams: FrameLayout.LayoutParams),
+        var prevIcons = NotificationIconsViewData()
+        sample(layoutParams, ::Pair).collect {
+            (iconsData: NotificationIconsViewData, layoutParams: FrameLayout.LayoutParams),
             ->
-            val iconsDiff = IconsViewData.computeDifference(iconsData, prevIcons)
+            val iconsDiff = NotificationIconsViewData.computeDifference(iconsData, prevIcons)
             prevIcons = iconsData
 
             val replacingIcons =
@@ -255,30 +313,27 @@
 
     // TODO(b/305739416): Once StatusBarIconView has its own Recommended Architecture stack, this
     //  can be moved there and cleaned up.
-    private fun applyTint(
-        view: NotificationIconContainer,
-        iconColors: IconColors,
+    private fun ViewGroup.applyTint(
+        iconColors: NotificationIconColors,
         contrastColorUtil: ContrastColorUtil,
     ) {
-        view.children
+        children
             .filterIsInstance<StatusBarIconView>()
             .filter { it.width != 0 }
-            .forEach { iv -> updateTintForIcon(iv, iconColors, contrastColorUtil) }
+            .forEach { iv -> iv.updateTintForIcon(iconColors, contrastColorUtil) }
     }
 
-    private fun updateTintForIcon(
-        v: StatusBarIconView,
-        iconColors: IconColors,
+    private fun StatusBarIconView.updateTintForIcon(
+        iconColors: NotificationIconColors,
         contrastColorUtil: ContrastColorUtil,
     ) {
-        val isPreL = java.lang.Boolean.TRUE == v.getTag(R.id.icon_is_pre_L)
-        val isColorized = !isPreL || NotificationUtils.isGrayscale(v, contrastColorUtil)
-        v.staticDrawableColor = iconColors.staticDrawableColor(v.viewBounds, isColorized)
-        v.setDecorColor(iconColors.tint)
+        val isPreL = java.lang.Boolean.TRUE == getTag(R.id.icon_is_pre_L)
+        val isColorized = !isPreL || NotificationUtils.isGrayscale(this, contrastColorUtil)
+        staticDrawableColor = iconColors.staticDrawableColor(viewBounds, isColorized)
+        setDecorColor(iconColors.tint)
     }
 
-    private suspend fun bindVisibility(
-        viewModel: NotificationIconContainerViewModel,
+    private suspend fun Flow<AnimatedValue<Boolean>>.bindIsVisible(
         view: NotificationIconContainer,
         configuration: ConfigurationState,
         featureFlags: FeatureFlagsClassic,
@@ -287,7 +342,7 @@
         val iconAppearTranslation =
             configuration.getDimensionPixelSize(R.dimen.shelf_appear_translation).stateIn(this)
         val statusViewMigrated = featureFlags.isEnabled(Flags.MIGRATE_KEYGUARD_STATUS_VIEW)
-        viewModel.isVisible.collect { isVisible ->
+        collect { isVisible ->
             view.animate().cancel()
             val animatorListener =
                 object : AnimatorListenerAdapter() {
@@ -304,7 +359,7 @@
                     view.visibility = if (isVisible.value) View.VISIBLE else View.INVISIBLE
                 }
                 featureFlags.isEnabled(Flags.NEW_AOD_TRANSITION) -> {
-                    animateInIconTranslation(view, statusViewMigrated)
+                    view.animateInIconTranslation(statusViewMigrated)
                     if (isVisible.value) {
                         CrossFadeHelper.fadeIn(view, animatorListener)
                     } else {
@@ -313,15 +368,14 @@
                 }
                 !isVisible.value -> {
                     // Let's make sure the icon are translated to 0, since we cancelled it above
-                    animateInIconTranslation(view, statusViewMigrated)
+                    view.animateInIconTranslation(statusViewMigrated)
                     CrossFadeHelper.fadeOut(view, animatorListener)
                 }
                 view.visibility != View.VISIBLE -> {
                     // No fading here, let's just appear the icons instead!
                     view.visibility = View.VISIBLE
                     view.alpha = 1f
-                    appearIcons(
-                        view,
+                    view.appearIcons(
                         animate = screenOffAnimationController.shouldAnimateAodIcons(),
                         iconAppearTranslation.value,
                         statusViewMigrated,
@@ -330,7 +384,7 @@
                 }
                 else -> {
                     // Let's make sure the icons are translated to 0, since we cancelled it above
-                    animateInIconTranslation(view, statusViewMigrated)
+                    view.animateInIconTranslation(statusViewMigrated)
                     // We were fading out, let's fade in instead
                     CrossFadeHelper.fadeIn(view, animatorListener)
                 }
@@ -338,8 +392,7 @@
         }
     }
 
-    private fun appearIcons(
-        view: View,
+    private fun View.appearIcons(
         animate: Boolean,
         iconAppearTranslation: Int,
         statusViewMigrated: Boolean,
@@ -347,11 +400,10 @@
     ) {
         if (animate) {
             if (!statusViewMigrated) {
-                view.translationY = -iconAppearTranslation.toFloat()
+                translationY = -iconAppearTranslation.toFloat()
             }
-            view.alpha = 0f
-            view
-                .animate()
+            alpha = 0f
+            animate()
                 .alpha(1f)
                 .setInterpolator(Interpolators.LINEAR)
                 .setDuration(AOD_ICONS_APPEAR_DURATION)
@@ -359,40 +411,29 @@
                 .setListener(animatorListener)
                 .start()
         } else {
-            view.alpha = 1.0f
+            alpha = 1.0f
             if (!statusViewMigrated) {
-                view.translationY = 0f
+                translationY = 0f
             }
         }
     }
 
-    private fun animateInIconTranslation(view: View, statusViewMigrated: Boolean) {
+    private fun View.animateInIconTranslation(statusViewMigrated: Boolean) {
         if (!statusViewMigrated) {
-            view.animate().animateInIconTranslation().setDuration(AOD_ICONS_APPEAR_DURATION).start()
+            animate().animateInIconTranslation().setDuration(AOD_ICONS_APPEAR_DURATION).start()
         }
     }
 
     private fun ViewPropertyAnimator.animateInIconTranslation(): ViewPropertyAnimator =
         setInterpolator(Interpolators.DECELERATE_QUINT).translationY(0f)
 
-    private const val AOD_ICONS_APPEAR_DURATION: Long = 200
-
-    private val View.viewBounds: Rect
-        get() {
-            val tmpArray = intArrayOf(0, 0)
-            getLocationOnScreen(tmpArray)
-            return Rect(
-                /* left = */ tmpArray[0],
-                /* top = */ tmpArray[1],
-                /* right = */ left + width,
-                /* bottom = */ top + height,
-            )
-        }
-
     /** External storage for [StatusBarIconView] instances. */
     fun interface IconViewStore {
         fun iconView(key: String): StatusBarIconView?
     }
+
+    private const val AOD_ICONS_APPEAR_DURATION: Long = 200
+    @ColorInt private val DEFAULT_AOD_ICON_COLOR = Color.WHITE
 }
 
 /** [IconViewStore] for the [com.android.systemui.statusbar.NotificationShelf] */
@@ -424,3 +465,15 @@
     override fun iconView(key: String): StatusBarIconView? =
         notifCollection.getEntry(key)?.icons?.statusBarIcon
 }
+
+private val View.viewBounds: Rect
+    get() {
+        val tmpArray = intArrayOf(0, 0)
+        getLocationOnScreen(tmpArray)
+        return Rect(
+            /* left = */ tmpArray[0],
+            /* top = */ tmpArray[1],
+            /* right = */ left + width,
+            /* bottom = */ top + height,
+        )
+    }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconColors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconColors.kt
new file mode 100644
index 0000000..97d1e1b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconColors.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ *
+ */
+
+package com.android.systemui.statusbar.notification.icon.ui.viewmodel
+
+import android.graphics.Rect
+
+/**
+ * Lookup the colors to use for the notification icons based on the bounds of the icon container. A
+ * result of `null` indicates that no color changes should be applied.
+ */
+fun interface NotificationIconColorLookup {
+    fun iconColors(viewBounds: Rect): NotificationIconColors?
+}
+
+/** Colors to apply to notification icons. */
+interface NotificationIconColors {
+
+    /** A tint to apply to the icons. */
+    val tint: Int
+
+    /**
+     * Returns the color to be applied to an icon, based on that icon's view bounds and whether or
+     * not the notification icon is colorized.
+     */
+    fun staticDrawableColor(viewBounds: Rect, isColorized: Boolean): Int
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
index 120d342..611ed89 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
@@ -15,10 +15,7 @@
  */
 package com.android.systemui.statusbar.notification.icon.ui.viewmodel
 
-import android.graphics.Color
 import android.graphics.Rect
-import androidx.annotation.ColorInt
-import com.android.systemui.common.ui.ConfigurationState
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
 import com.android.systemui.flags.FeatureFlagsClassic
@@ -27,14 +24,9 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.res.R
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor
 import com.android.systemui.statusbar.notification.icon.domain.interactor.AlwaysOnDisplayNotificationIconsInteractor
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.ColorLookup
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.IconColors
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.IconInfo
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.IconsViewData
 import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
 import com.android.systemui.util.kotlin.pairwise
@@ -47,8 +39,6 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 
 /** View-model for the row of notification icons displayed on the always-on display. */
@@ -56,7 +46,6 @@
 class NotificationIconContainerAlwaysOnDisplayViewModel
 @Inject
 constructor(
-    configuration: ConfigurationState,
     private val deviceEntryInteractor: DeviceEntryInteractor,
     private val dozeParameters: DozeParameters,
     private val featureFlags: FeatureFlagsClassic,
@@ -66,14 +55,10 @@
     private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor,
     screenOffAnimationController: ScreenOffAnimationController,
     shadeInteractor: ShadeInteractor,
-) : NotificationIconContainerViewModel {
+) {
 
-    override val iconColors: Flow<ColorLookup> =
-        configuration.getColorAttr(R.attr.wallpaperTextColor, DEFAULT_AOD_ICON_COLOR).map { tint ->
-            ColorLookup { IconColorsImpl(tint) }
-        }
-
-    override val animationsEnabled: Flow<Boolean> =
+    /** Are changes to the icon container animated? */
+    val animationsEnabled: Flow<Boolean> =
         combine(
             shadeInteractor.isShadeTouchable,
             keyguardInteractor.isKeyguardVisible,
@@ -81,7 +66,8 @@
             panelTouchesEnabled && isKeyguardVisible
         }
 
-    override val isDozing: Flow<AnimatedValue<Boolean>> =
+    /** Should icons be rendered in "dozing" mode? */
+    val isDozing: Flow<AnimatedValue<Boolean>> =
         keyguardTransitionInteractor.startedKeyguardTransitionStep
             // Determine if we're dozing based on the most recent transition
             .map { step: TransitionStep ->
@@ -98,7 +84,8 @@
             .distinctUntilChanged()
             .toAnimatedValueFlow()
 
-    override val isVisible: Flow<AnimatedValue<Boolean>> =
+    /** Is the icon container visible? */
+    val isVisible: Flow<AnimatedValue<Boolean>> =
         combine(
                 keyguardTransitionInteractor.finishedKeyguardState.map { it != KeyguardState.GONE },
                 deviceEntryInteractor.isBypassEnabled,
@@ -136,17 +123,14 @@
             }
             .distinctUntilChanged()
 
-    override val iconsViewData: Flow<IconsViewData> =
+    /** [NotificationIconsViewData] indicating which icons to display in the view. */
+    val icons: Flow<NotificationIconsViewData> =
         iconsInteractor.aodNotifs.map { entries ->
-            IconsViewData(
+            NotificationIconsViewData(
                 visibleKeys = entries.mapNotNull { it.toIconInfo(it.aodIcon) },
             )
         }
 
-    override val isolatedIcon: Flow<AnimatedValue<IconInfo?>> =
-        flowOf(AnimatedValue.NotAnimating(null))
-    override val isolatedIconLocation: Flow<Rect> = emptyFlow()
-
     /** Is there an expanded pulse, are we animating in response? */
     private fun isPulseExpandingAnimated(): Flow<AnimatedValue<Boolean>> {
         return notificationsKeyguardInteractor.isPulseExpanding
@@ -182,11 +166,7 @@
             .toAnimatedValueFlow()
     }
 
-    private class IconColorsImpl(override val tint: Int) : IconColors {
+    private class IconColorsImpl(override val tint: Int) : NotificationIconColors {
         override fun staticDrawableColor(viewBounds: Rect, isColorized: Boolean): Int = tint
     }
-
-    companion object {
-        @ColorInt private val DEFAULT_AOD_ICON_COLOR = Color.WHITE
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
index c6aabb7..1560106 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
@@ -15,16 +15,9 @@
  */
 package com.android.systemui.statusbar.notification.icon.ui.viewmodel
 
-import android.graphics.Rect
 import com.android.systemui.statusbar.notification.icon.domain.interactor.NotificationIconsInteractor
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.ColorLookup
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.IconInfo
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.IconsViewData
-import com.android.systemui.util.ui.AnimatedValue
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 
 /** View-model for the overflow row of notification icons displayed in the notification shade. */
@@ -32,19 +25,11 @@
 @Inject
 constructor(
     interactor: NotificationIconsInteractor,
-) : NotificationIconContainerViewModel {
-
-    override val animationsEnabled: Flow<Boolean> = flowOf(true)
-    override val isDozing: Flow<AnimatedValue<Boolean>> = emptyFlow()
-    override val isVisible: Flow<AnimatedValue<Boolean>> = emptyFlow()
-    override val iconColors: Flow<ColorLookup> = emptyFlow()
-    override val isolatedIcon: Flow<AnimatedValue<IconInfo?>> =
-        flowOf(AnimatedValue.NotAnimating(null))
-    override val isolatedIconLocation: Flow<Rect> = emptyFlow()
-
-    override val iconsViewData: Flow<IconsViewData> =
+) {
+    /** [NotificationIconsViewData] indicating which icons to display in the view. */
+    val icons: Flow<NotificationIconsViewData> =
         interactor.filteredNotifSet().map { entries ->
-            IconsViewData(
+            NotificationIconsViewData(
                 visibleKeys = entries.mapNotNull { it.toIconInfo(it.shelfIcon) },
             )
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
index 4d14024..53631e3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
@@ -22,10 +22,6 @@
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationIconInteractor
 import com.android.systemui.statusbar.notification.icon.domain.interactor.StatusBarNotificationIconsInteractor
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.ColorLookup
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.IconColors
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.IconInfo
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.IconsViewData
 import com.android.systemui.statusbar.phone.domain.interactor.DarkIconInteractor
 import com.android.systemui.util.kotlin.pairwise
 import com.android.systemui.util.kotlin.sample
@@ -35,7 +31,6 @@
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.map
 
@@ -49,8 +44,10 @@
     keyguardInteractor: KeyguardInteractor,
     notificationsInteractor: ActiveNotificationsInteractor,
     shadeInteractor: ShadeInteractor,
-) : NotificationIconContainerViewModel {
-    override val animationsEnabled: Flow<Boolean> =
+) {
+
+    /** Are changes to the icon container animated? */
+    val animationsEnabled: Flow<Boolean> =
         combine(
             shadeInteractor.isShadeTouchable,
             keyguardInteractor.isKeyguardShowing,
@@ -58,14 +55,15 @@
             panelTouchesEnabled && !isKeyguardShowing
         }
 
-    override val iconColors: Flow<ColorLookup> =
+    /** The colors with which to display the notification icons. */
+    val iconColors: Flow<NotificationIconColorLookup> =
         combine(
             darkIconInteractor.tintAreas,
             darkIconInteractor.tintColor,
             // Included so that tints are re-applied after entries are changed.
             notificationsInteractor.notifications,
         ) { areas, tint, _ ->
-            ColorLookup { viewBounds: Rect ->
+            NotificationIconColorLookup { viewBounds: Rect ->
                 if (DarkIconDispatcher.isInAreas(areas, viewBounds)) {
                     IconColorsImpl(tint, areas)
                 } else {
@@ -74,20 +72,19 @@
             }
         }
 
-    override val isDozing: Flow<AnimatedValue<Boolean>> = emptyFlow()
-    override val isVisible: Flow<AnimatedValue<Boolean>> = emptyFlow()
-
-    override val iconsViewData: Flow<IconsViewData> =
+    /** [NotificationIconsViewData] indicating which icons to display in the view. */
+    val icons: Flow<NotificationIconsViewData> =
         iconsInteractor.statusBarNotifs.map { entries ->
-            IconsViewData(
+            NotificationIconsViewData(
                 visibleKeys = entries.mapNotNull { it.toIconInfo(it.statusBarIcon) },
             )
         }
 
-    override val isolatedIcon: Flow<AnimatedValue<IconInfo?>> =
+    /** An Icon to show "isolated" in the IconContainer. */
+    val isolatedIcon: Flow<AnimatedValue<NotificationIconInfo?>> =
         headsUpIconInteractor.isolatedNotification
             .pairwise(initialValue = null)
-            .sample(combine(iconsViewData, shadeInteractor.shadeExpansion, ::Pair)) {
+            .sample(combine(icons, shadeInteractor.shadeExpansion, ::Pair)) {
                 (prev, isolatedNotif),
                 (iconsViewData, shadeExpansion),
                 ->
@@ -105,13 +102,14 @@
             }
             .toAnimatedValueFlow()
 
-    override val isolatedIconLocation: Flow<Rect> =
+    /** Location to show an isolated icon, if there is one. */
+    val isolatedIconLocation: Flow<Rect> =
         headsUpIconInteractor.isolatedIconLocation.filterNotNull()
 
     private class IconColorsImpl(
         override val tint: Int,
         private val areas: Collection<Rect>,
-    ) : IconColors {
+    ) : NotificationIconColors {
         override fun staticDrawableColor(viewBounds: Rect, isColorized: Boolean): Int {
             return if (isColorized && DarkIconDispatcher.isInAreas(areas, viewBounds)) {
                 tint
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerViewModel.kt
deleted file mode 100644
index a611323..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerViewModel.kt
+++ /dev/null
@@ -1,196 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.systemui.statusbar.notification.icon.ui.viewmodel
-
-import android.graphics.Rect
-import android.graphics.drawable.Icon
-import androidx.collection.ArrayMap
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel.IconInfo
-import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
-import com.android.systemui.util.kotlin.mapValuesNotNullTo
-import com.android.systemui.util.ui.AnimatedValue
-import kotlinx.coroutines.flow.Flow
-
-/**
- * View-model for the row of notification icons displayed in the NotificationShelf, StatusBar, and
- * AOD.
- */
-interface NotificationIconContainerViewModel {
-
-    /** Are changes to the icon container animated? */
-    val animationsEnabled: Flow<Boolean>
-
-    /** Should icons be rendered in "dozing" mode? */
-    val isDozing: Flow<AnimatedValue<Boolean>>
-
-    /** Is the icon container visible? */
-    val isVisible: Flow<AnimatedValue<Boolean>>
-
-    /** The colors with which to display the notification icons. */
-    val iconColors: Flow<ColorLookup>
-
-    /** [IconsViewData] indicating which icons to display in the view. */
-    val iconsViewData: Flow<IconsViewData>
-
-    /** An Icon to show "isolated" in the IconContainer. */
-    val isolatedIcon: Flow<AnimatedValue<IconInfo?>>
-
-    /** Location to show an isolated icon, if there is one. */
-    val isolatedIconLocation: Flow<Rect>
-
-    /**
-     * Lookup the colors to use for the notification icons based on the bounds of the icon
-     * container. A result of `null` indicates that no color changes should be applied.
-     */
-    fun interface ColorLookup {
-        fun iconColors(viewBounds: Rect): IconColors?
-    }
-
-    /** Colors to apply to notification icons. */
-    interface IconColors {
-
-        /** A tint to apply to the icons. */
-        val tint: Int
-
-        /**
-         * Returns the color to be applied to an icon, based on that icon's view bounds and whether
-         * or not the notification icon is colorized.
-         */
-        fun staticDrawableColor(viewBounds: Rect, isColorized: Boolean): Int
-    }
-
-    /** Encapsulates the collection of notification icons present on the device. */
-    data class IconsViewData(
-        /** Icons that are visible in the container. */
-        val visibleKeys: List<IconInfo> = emptyList(),
-        /** Keys of icons that are "behind" the overflow dot. */
-        val collapsedKeys: Set<String> = emptySet(),
-        /** Whether the overflow dot should be shown regardless if [collapsedKeys] is empty. */
-        val forceShowDot: Boolean = false,
-    ) {
-        /** The difference between two [IconsViewData]s. */
-        data class Diff(
-            /** Icons added in the newer dataset. */
-            val added: List<IconInfo> = emptyList(),
-            /** Icons removed from the older dataset. */
-            val removed: List<String> = emptyList(),
-            /**
-             * Groups whose icon was replaced with a single new notification icon. The key of the
-             * [Map] is the notification group key, and the value is the new icon.
-             *
-             * Specifically, this models a difference where the older dataset had notification
-             * groups with a single icon in the set, and the newer dataset has a single, different
-             * icon for the same group. A view binder can use this information for special
-             * animations for this specific change.
-             */
-            val groupReplacements: Map<String, IconInfo> = emptyMap(),
-        )
-
-        companion object {
-            /**
-             * Returns an [IconsViewData.Diff] calculated from a [new] and [previous][prev]
-             * [IconsViewData] state.
-             */
-            fun computeDifference(new: IconsViewData, prev: IconsViewData): Diff {
-                val added: List<IconInfo> =
-                    new.visibleKeys.filter {
-                        it.notifKey !in prev.visibleKeys.asSequence().map { it.notifKey }
-                    }
-                val removed: List<IconInfo> =
-                    prev.visibleKeys.filter {
-                        it.notifKey !in new.visibleKeys.asSequence().map { it.notifKey }
-                    }
-                val groupsToShow: Set<IconGroupInfo> =
-                    new.visibleKeys.asSequence().map { it.groupInfo }.toSet()
-                val replacements: ArrayMap<String, IconInfo> =
-                    removed
-                        .asSequence()
-                        .filter { keyToRemove -> keyToRemove.groupInfo in groupsToShow }
-                        .groupBy { it.groupInfo.groupKey }
-                        .mapValuesNotNullTo(ArrayMap()) { (_, vs) ->
-                            vs.takeIf { it.size == 1 }?.get(0)
-                        }
-                return Diff(added, removed.map { it.notifKey }, replacements)
-            }
-        }
-    }
-
-    /** An Icon, and keys for unique identification. */
-    data class IconInfo(
-        val sourceIcon: Icon,
-        val notifKey: String,
-        val groupKey: String,
-    )
-}
-
-/**
- * Construct an [IconInfo] out of an [ActiveNotificationModel], or return `null` if one cannot be
- * created due to missing information.
- */
-fun ActiveNotificationModel.toIconInfo(sourceIcon: Icon?): IconInfo? {
-    return sourceIcon?.let {
-        groupKey?.let { groupKey ->
-            IconInfo(
-                sourceIcon = sourceIcon,
-                notifKey = key,
-                groupKey = groupKey,
-            )
-        }
-    }
-}
-
-private val IconInfo.groupInfo: IconGroupInfo
-    get() = IconGroupInfo(sourceIcon, groupKey)
-
-private data class IconGroupInfo(
-    val sourceIcon: Icon,
-    val groupKey: String,
-) {
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (javaClass != other?.javaClass) return false
-
-        other as IconGroupInfo
-
-        if (groupKey != other.groupKey) return false
-        return sourceIcon.sameAs(other.sourceIcon)
-    }
-
-    override fun hashCode(): Int {
-        var result = groupKey.hashCode()
-        result = 31 * result + sourceIcon.type.hashCode()
-        when (sourceIcon.type) {
-            Icon.TYPE_BITMAP,
-            Icon.TYPE_ADAPTIVE_BITMAP -> {
-                result = 31 * result + sourceIcon.bitmap.hashCode()
-            }
-            Icon.TYPE_DATA -> {
-                result = 31 * result + sourceIcon.dataLength.hashCode()
-                result = 31 * result + sourceIcon.dataOffset.hashCode()
-            }
-            Icon.TYPE_RESOURCE -> {
-                result = 31 * result + sourceIcon.resId.hashCode()
-                result = 31 * result + sourceIcon.resPackage.hashCode()
-            }
-            Icon.TYPE_URI,
-            Icon.TYPE_URI_ADAPTIVE_BITMAP -> {
-                result = 31 * result + sourceIcon.uriString.hashCode()
-            }
-        }
-        return result
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconsViewData.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconsViewData.kt
new file mode 100644
index 0000000..867be84
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconsViewData.kt
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ *
+ */
+
+package com.android.systemui.statusbar.notification.icon.ui.viewmodel
+
+import android.graphics.drawable.Icon
+import androidx.collection.ArrayMap
+import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
+import com.android.systemui.util.kotlin.mapValuesNotNullTo
+
+/** Encapsulates the collection of notification icons present on the device. */
+data class NotificationIconsViewData(
+    /** Icons that are visible in the container. */
+    val visibleKeys: List<NotificationIconInfo> = emptyList(),
+    /** Keys of icons that are "behind" the overflow dot. */
+    val collapsedKeys: Set<String> = emptySet(),
+    /** Whether the overflow dot should be shown regardless if [collapsedKeys] is empty. */
+    val forceShowDot: Boolean = false,
+) {
+    /** The difference between two [NotificationIconsViewData]s. */
+    data class Diff(
+        /** Icons added in the newer dataset. */
+        val added: List<NotificationIconInfo> = emptyList(),
+        /** Icons removed from the older dataset. */
+        val removed: List<String> = emptyList(),
+        /**
+         * Groups whose icon was replaced with a single new notification icon. The key of the [Map]
+         * is the notification group key, and the value is the new icon.
+         *
+         * Specifically, this models a difference where the older dataset had notification groups
+         * with a single icon in the set, and the newer dataset has a single, different icon for the
+         * same group. A view binder can use this information for special animations for this
+         * specific change.
+         */
+        val groupReplacements: Map<String, NotificationIconInfo> = emptyMap(),
+    )
+
+    companion object {
+        /**
+         * Returns an [NotificationIconsViewData.Diff] calculated from a [new] and [previous][prev]
+         * [NotificationIconsViewData] state.
+         */
+        fun computeDifference(
+            new: NotificationIconsViewData,
+            prev: NotificationIconsViewData
+        ): Diff {
+            val added: List<NotificationIconInfo> =
+                new.visibleKeys.filter {
+                    it.notifKey !in prev.visibleKeys.asSequence().map { it.notifKey }
+                }
+            val removed: List<NotificationIconInfo> =
+                prev.visibleKeys.filter {
+                    it.notifKey !in new.visibleKeys.asSequence().map { it.notifKey }
+                }
+            val groupsToShow: Set<IconGroupInfo> =
+                new.visibleKeys.asSequence().map { it.groupInfo }.toSet()
+            val replacements: ArrayMap<String, NotificationIconInfo> =
+                removed
+                    .asSequence()
+                    .filter { keyToRemove -> keyToRemove.groupInfo in groupsToShow }
+                    .groupBy { it.groupInfo.groupKey }
+                    .mapValuesNotNullTo(ArrayMap()) { (_, vs) ->
+                        vs.takeIf { it.size == 1 }?.get(0)
+                    }
+            return Diff(added, removed.map { it.notifKey }, replacements)
+        }
+    }
+}
+
+/** An Icon, and keys for unique identification. */
+data class NotificationIconInfo(
+    val sourceIcon: Icon,
+    val notifKey: String,
+    val groupKey: String,
+)
+
+/**
+ * Construct an [NotificationIconInfo] out of an [ActiveNotificationModel], or return `null` if one
+ * cannot be created due to missing information.
+ */
+fun ActiveNotificationModel.toIconInfo(sourceIcon: Icon?): NotificationIconInfo? {
+    return sourceIcon?.let {
+        groupKey?.let { groupKey ->
+            NotificationIconInfo(
+                sourceIcon = sourceIcon,
+                notifKey = key,
+                groupKey = groupKey,
+            )
+        }
+    }
+}
+
+private val NotificationIconInfo.groupInfo: IconGroupInfo
+    get() = IconGroupInfo(sourceIcon, groupKey)
+
+private data class IconGroupInfo(
+    val sourceIcon: Icon,
+    val groupKey: String,
+) {
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+
+        other as IconGroupInfo
+
+        if (groupKey != other.groupKey) return false
+        return sourceIcon.sameAs(other.sourceIcon)
+    }
+
+    override fun hashCode(): Int {
+        var result = groupKey.hashCode()
+        result = 31 * result + sourceIcon.type.hashCode()
+        when (sourceIcon.type) {
+            Icon.TYPE_BITMAP,
+            Icon.TYPE_ADAPTIVE_BITMAP -> {
+                result = 31 * result + sourceIcon.bitmap.hashCode()
+            }
+            Icon.TYPE_DATA -> {
+                result = 31 * result + sourceIcon.dataLength.hashCode()
+                result = 31 * result + sourceIcon.dataOffset.hashCode()
+            }
+            Icon.TYPE_RESOURCE -> {
+                result = 31 * result + sourceIcon.resId.hashCode()
+                result = 31 * result + sourceIcon.resPackage.hashCode()
+            }
+            Icon.TYPE_URI,
+            Icon.TYPE_URI_ADAPTIVE_BITMAP -> {
+                result = 31 * result + sourceIcon.uriString.hashCode()
+            }
+        }
+        return result
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
index 4a823a4..2fffd37 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
@@ -239,11 +239,11 @@
         })
     }
 
-    fun logNoPulsingNotificationHidden(entry: NotificationEntry) {
+    fun logNoPulsingNotificationHiddenOverride(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
             str1 = entry.logKey
         }, {
-            "No pulsing: notification hidden on lock screen: $str1"
+            "No pulsing: notification hidden on lock screen by override: $str1"
         })
     }
 
@@ -290,11 +290,11 @@
         })
     }
 
-    fun keyguardHideNotification(entry: NotificationEntry) {
+    fun logNoAlertingNotificationHidden(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
             str1 = entry.logKey
         }, {
-            "Keyguard Hide Notification: $str1"
+            "No alerting: notification hidden on lock screen: $str1"
         })
     }
 }
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 6ec9dbe..b0155f1 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
@@ -180,4 +180,9 @@
      * Add a component that can suppress visual interruptions.
      */
     void addSuppressor(NotificationInterruptSuppressor suppressor);
+
+    /**
+     * Remove a component that can suppress visual interruptions.
+     */
+    void removeSuppressor(NotificationInterruptSuppressor suppressor);
 }
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 3819843..301ddbf 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
@@ -175,6 +175,11 @@
     }
 
     @Override
+    public void removeSuppressor(NotificationInterruptSuppressor suppressor) {
+        mSuppressors.remove(suppressor);
+    }
+
+    @Override
     public boolean shouldBubbleUp(NotificationEntry entry) {
         final StatusBarNotification sbn = entry.getSbn();
 
@@ -505,7 +510,7 @@
 
         if (entry.getRanking().getLockscreenVisibilityOverride()
                 == Notification.VISIBILITY_PRIVATE) {
-            if (log) mLogger.logNoPulsingNotificationHidden(entry);
+            if (log) mLogger.logNoPulsingNotificationHiddenOverride(entry);
             return false;
         }
 
@@ -536,7 +541,7 @@
         }
 
         if (mKeyguardNotificationVisibilityProvider.shouldHideNotification(entry)) {
-            if (log) mLogger.keyguardHideNotification(entry);
+            if (log) mLogger.logNoAlertingNotificationHidden(entry);
             return false;
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapper.kt
index ebdeded..d7f0baf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapper.kt
@@ -58,6 +58,10 @@
         wrapped.addSuppressor(suppressor)
     }
 
+    override fun removeLegacySuppressor(suppressor: NotificationInterruptSuppressor) {
+        wrapped.removeSuppressor(suppressor)
+    }
+
     override fun makeUnloggedHeadsUpDecision(entry: NotificationEntry): Decision =
         wrapped.checkHeadsUp(entry, /* log= */ false).let { DecisionImpl.of(it) }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProvider.kt
index 454ba02..920bbe9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProvider.kt
@@ -60,6 +60,13 @@
     fun addLegacySuppressor(suppressor: NotificationInterruptSuppressor)
 
     /**
+     * Removes a [component][suppressor] that can suppress visual interruptions.
+     *
+     * @param[suppressor] the suppressor to remove
+     */
+    fun removeLegacySuppressor(suppressor: NotificationInterruptSuppressor)
+
+    /**
      * Decides whether a [notification][entry] should display as heads-up or not, but does not log
      * that decision.
      *
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt
new file mode 100644
index 0000000..4ef80e3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.interruption
+
+import com.android.internal.logging.UiEventLogger.UiEventEnum
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+
+/**
+ * A reason why visual interruptions might be suppressed.
+ *
+ * @see VisualInterruptionCondition
+ * @see VisualInterruptionFilter
+ */
+enum class VisualInterruptionType {
+    /* HUN when awake */
+    PEEK,
+
+    /* HUN when dozing */
+    PULSE,
+
+    /* Bubble */
+    BUBBLE
+}
+
+/**
+ * A reason why visual interruptions might be suppressed.
+ *
+ * @see VisualInterruptionCondition
+ * @see VisualInterruptionFilter
+ */
+sealed interface VisualInterruptionSuppressor {
+    /** The type(s) of interruption that this suppresses. */
+    val types: Set<VisualInterruptionType>
+
+    /** A human-readable string to be logged to explain why this suppressed an interruption. */
+    val reason: String
+
+    /** An optional UiEvent ID to be recorded when this suppresses an interruption. */
+    val uiEventId: UiEventEnum?
+}
+
+/** A reason why visual interruptions might be suppressed regardless of the notification. */
+abstract class VisualInterruptionCondition(
+    override val types: Set<VisualInterruptionType>,
+    override val reason: String,
+    override val uiEventId: UiEventEnum? = null
+) : VisualInterruptionSuppressor {
+    /** @return true if these interruptions should be suppressed right now. */
+    abstract fun shouldSuppress(): Boolean
+}
+
+/** A reason why visual interruptions might be suppressed based on the notification. */
+abstract class VisualInterruptionFilter(
+    override val types: Set<VisualInterruptionType>,
+    override val reason: String,
+    override val uiEventId: UiEventEnum? = null
+) : VisualInterruptionSuppressor {
+    /**
+     * @param entry the notification to consider suppressing
+     * @return true if these interruptions should be suppressed for this notification right now
+     */
+    abstract fun shouldSuppress(entry: NotificationEntry): Boolean
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationIconContainerRefactor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationIconContainerRefactor.kt
new file mode 100644
index 0000000..dee609c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationIconContainerRefactor.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ *
+ */
+
+package com.android.systemui.statusbar.notification.shared
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the NotificationIconContainer refactor flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object NotificationIconContainerRefactor {
+    const val FLAG_NAME = Flags.FLAG_NOTIFICATIONS_ICON_CONTAINER_REFACTOR
+
+    /** Is the refactor enabled? */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.notificationsIconContainerRefactor()
+
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun isUnexpectedlyInLegacyMode() =
+        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
index b92c51f..d35e4b5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
@@ -19,19 +19,23 @@
 import android.view.View
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.common.ui.ConfigurationState
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.statusbar.LegacyNotificationShelfControllerImpl
 import com.android.systemui.statusbar.NotificationShelf
 import com.android.systemui.statusbar.NotificationShelfController
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ShelfNotificationIconViewStore
 import com.android.systemui.statusbar.notification.row.ui.viewbinder.ActivatableNotificationViewBinder
+import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
 import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel
 import com.android.systemui.statusbar.notification.stack.AmbientState
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.phone.NotificationIconAreaController
 import com.android.systemui.statusbar.phone.NotificationIconContainer
+import com.android.systemui.statusbar.policy.ConfigurationController
 import javax.inject.Inject
 import kotlinx.coroutines.awaitCancellation
 import kotlinx.coroutines.launch
@@ -75,14 +79,25 @@
     fun bind(
         shelf: NotificationShelf,
         viewModel: NotificationShelfViewModel,
+        configuration: ConfigurationState,
+        configurationController: ConfigurationController,
         falsingManager: FalsingManager,
-        featureFlags: FeatureFlags,
         notificationIconAreaController: NotificationIconAreaController,
+        shelfIconViewStore: ShelfNotificationIconViewStore,
     ) {
         ActivatableNotificationViewBinder.bind(viewModel, shelf, falsingManager)
         shelf.apply {
-            // TODO(278765923): Replace with eventual NotificationIconContainerViewBinder#bind()
-            notificationIconAreaController.setShelfIcons(shelfIcons)
+            if (NotificationIconContainerRefactor.isEnabled) {
+                NotificationIconContainerViewBinder.bind(
+                    shelfIcons,
+                    viewModel.icons,
+                    configuration,
+                    configurationController,
+                    shelfIconViewStore,
+                )
+            } else {
+                notificationIconAreaController.setShelfIcons(shelfIcons)
+            }
             repeatWhenAttached {
                 repeatOnLifecycle(Lifecycle.State.STARTED) {
                     launch {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt
index 5ca8b53..64b5b62c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt
@@ -18,6 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.NotificationShelf
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerShelfViewModel
 import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModel
 import com.android.systemui.statusbar.notification.shelf.domain.interactor.NotificationShelfInteractor
 import javax.inject.Inject
@@ -31,6 +32,7 @@
 constructor(
     private val interactor: NotificationShelfInteractor,
     activatableViewModel: ActivatableNotificationViewModel,
+    val icons: NotificationIconContainerShelfViewModel,
 ) : ActivatableNotificationViewModel by activatableViewModel {
     /** Is the shelf allowed to be clickable when it has content? */
     val isClickable: Flow<Boolean>
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 79448b4..21efd63 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -59,6 +59,7 @@
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.classifier.Classifier;
 import com.android.systemui.classifier.FalsingCollector;
+import com.android.systemui.common.ui.ConfigurationState;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
@@ -107,6 +108,7 @@
 import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController;
 import com.android.systemui.statusbar.notification.dagger.SilentHeader;
 import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor;
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ShelfNotificationIconViewStore;
 import com.android.systemui.statusbar.notification.init.NotificationsController;
 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
@@ -117,10 +119,12 @@
 import com.android.systemui.statusbar.notification.row.NotificationSnooze;
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder;
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel;
+import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
 import com.android.systemui.statusbar.phone.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
+import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
 import com.android.systemui.statusbar.phone.ScrimController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
@@ -212,6 +216,8 @@
     private final SecureSettings mSecureSettings;
     private final NotificationDismissibilityProvider mDismissibilityProvider;
     private final ActivityStarter mActivityStarter;
+    private final ConfigurationState mConfigurationState;
+    private final ShelfNotificationIconViewStore mShelfIconViewStore;
 
     private View mLongPressedView;
 
@@ -674,7 +680,9 @@
             SecureSettings secureSettings,
             NotificationDismissibilityProvider dismissibilityProvider,
             ActivityStarter activityStarter,
-            SplitShadeStateController splitShadeStateController) {
+            SplitShadeStateController splitShadeStateController,
+            ConfigurationState configurationState,
+            ShelfNotificationIconViewStore shelfIconViewStore) {
         mView = view;
         mKeyguardTransitionRepo = keyguardTransitionRepo;
         mStackStateLogger = stackLogger;
@@ -724,6 +732,8 @@
         mSecureSettings = secureSettings;
         mDismissibilityProvider = dismissibilityProvider;
         mActivityStarter = activityStarter;
+        mConfigurationState = configurationState;
+        mShelfIconViewStore = shelfIconViewStore;
         mView.passSplitShadeStateController(splitShadeStateController);
         updateResources();
         setUpView();
@@ -832,8 +842,10 @@
 
         mViewModel.ifPresent(
                 vm -> NotificationListViewBinder
-                        .bind(mView, vm, mFalsingManager, mFeatureFlags, mNotifIconAreaController,
-                                mConfigurationController));
+                        .bind(mView, vm, mConfigurationState, mConfigurationController,
+                                mFalsingManager,
+                                mNotifIconAreaController,
+                                mShelfIconViewStore));
 
         collectFlow(mView, mKeyguardTransitionRepo.getTransitions(),
                 this::onKeyguardTransitionChanged);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index a3792cf..95b467f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -17,23 +17,21 @@
 package com.android.systemui.statusbar.notification.stack.ui.viewbinder
 
 import android.view.LayoutInflater
-import com.android.systemui.flags.FeatureFlagsClassic
+import com.android.systemui.common.ui.ConfigurationState
+import com.android.systemui.common.ui.reinflateAndBindLatest
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.NotificationShelf
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
 import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ShelfNotificationIconViewStore
 import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinder
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel
 import com.android.systemui.statusbar.phone.NotificationIconAreaController
 import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.statusbar.policy.onDensityOrFontScaleChanged
-import com.android.systemui.statusbar.policy.onThemeChanged
 import com.android.systemui.util.traceSection
-import com.android.systemui.util.view.reinflateAndBindLatest
-import kotlinx.coroutines.flow.merge
 
 /** Binds a [NotificationStackScrollLayout] to its [view model][NotificationListViewModel]. */
 object NotificationListViewBinder {
@@ -41,10 +39,11 @@
     fun bind(
         view: NotificationStackScrollLayout,
         viewModel: NotificationListViewModel,
-        falsingManager: FalsingManager,
-        featureFlags: FeatureFlagsClassic,
-        iconAreaController: NotificationIconAreaController,
+        configuration: ConfigurationState,
         configurationController: ConfigurationController,
+        falsingManager: FalsingManager,
+        iconAreaController: NotificationIconAreaController,
+        shelfIconViewStore: ShelfNotificationIconViewStore,
     ) {
         val shelf =
             LayoutInflater.from(view.context)
@@ -52,28 +51,24 @@
         NotificationShelfViewBinder.bind(
             shelf,
             viewModel.shelf,
+            configuration,
+            configurationController,
             falsingManager,
-            featureFlags,
-            iconAreaController
+            iconAreaController,
+            shelfIconViewStore,
         )
         view.setShelf(shelf)
 
         viewModel.footer.ifPresent { footerViewModel ->
             // The footer needs to be re-inflated every time the theme or the font size changes.
             view.repeatWhenAttached {
-                LayoutInflater.from(view.context).reinflateAndBindLatest(
+                configuration.reinflateAndBindLatest(
                     R.layout.status_bar_notification_footer,
                     view,
                     attachToRoot = false,
-                    // TODO(b/305930747): This may lead to duplicate invocations if both flows emit,
-                    // find a solution to only emit one event.
-                    merge(
-                        configurationController.onThemeChanged,
-                        configurationController.onDensityOrFontScaleChanged,
-                    ),
-                ) { view ->
+                ) { footerView: FooterView ->
                     traceSection("bind FooterView") {
-                        FooterViewBinder.bind(view as FooterView, footerViewModel)
+                        FooterViewBinder.bind(footerView, footerViewModel)
                     }
                 }
             }
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 8295f65..897bb42 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -206,6 +206,7 @@
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
+import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor;
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
@@ -236,8 +237,6 @@
 
 import dalvik.annotation.optimization.NeverCompile;
 
-import dagger.Lazy;
-
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.List;
@@ -249,6 +248,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.
@@ -809,8 +810,6 @@
                 mShadeExpansionStateManager.addExpansionListener(shadeExpansionListener);
         shadeExpansionListener.onPanelExpansionChanged(currentState);
 
-        mShadeExpansionStateManager.addFullExpansionListener(this::onShadeExpansionFullyChanged);
-
         mActivityIntentHelper = new ActivityIntentHelper(mContext);
         mActivityLaunchAnimator = activityLaunchAnimator;
 
@@ -1397,20 +1396,6 @@
         }
     }
 
-    @VisibleForTesting
-    void onShadeExpansionFullyChanged(Boolean isExpanded) {
-        if (isExpanded && mStatusBarStateController.getState() != StatusBarState.KEYGUARD) {
-            if (DEBUG) {
-                Log.v(TAG, "clearing notification effects from Height");
-            }
-            clearNotificationEffects();
-        }
-
-        if (!isExpanded) {
-            mRemoteInputManager.onPanelCollapsed();
-        }
-    }
-
     @NonNull
     @Override
     public Lifecycle getLifecycle() {
@@ -2635,7 +2620,7 @@
                 !mDozeServiceHost.isPulsing(), mDeviceProvisionedController.isFrpActive());
 
         mShadeSurface.setTouchAndAnimationDisabled(disabled);
-        if (!mFeatureFlags.isEnabled(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR)) {
+        if (!NotificationIconContainerRefactor.isEnabled()) {
             mNotificationIconAreaController.setAnimationsEnabled(!disabled);
         }
     }
@@ -3102,7 +3087,7 @@
             }
             // TODO: Bring these out of CentralSurfaces.
             mUserInfoControllerImpl.onDensityOrFontScaleChanged();
-            if (!mFeatureFlags.isEnabled(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR)) {
+            if (!NotificationIconContainerRefactor.isEnabled()) {
                 mNotificationIconAreaController.onDensityOrFontScaleChanged(mContext);
             }
         }
@@ -3122,7 +3107,7 @@
             if (mAmbientIndicationContainer instanceof AutoReinflateContainer) {
                 ((AutoReinflateContainer) mAmbientIndicationContainer).inflateLayout();
             }
-            if (!mFeatureFlags.isEnabled(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR)) {
+            if (!NotificationIconContainerRefactor.isEnabled()) {
                 mNotificationIconAreaController.onThemeChanged();
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
index 66341ba..600d4af 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
@@ -38,7 +38,6 @@
 import com.android.systemui.doze.DozeLog;
 import com.android.systemui.doze.DozeReceiver;
 import com.android.systemui.flags.FeatureFlagsClassic;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.keyguard.domain.interactor.DozeInteractor;
 import com.android.systemui.shade.NotificationShadeWindowViewController;
@@ -49,18 +48,18 @@
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor;
 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.OnHeadsUpChangedListener;
 import com.android.systemui.util.Assert;
 
-import dagger.Lazy;
-
 import java.util.ArrayList;
 
 import javax.inject.Inject;
 
+import dagger.Lazy;
 import kotlinx.coroutines.ExperimentalCoroutinesApi;
 
 /**
@@ -178,7 +177,7 @@
 
     void fireNotificationPulse(NotificationEntry entry) {
         Runnable pulseSuppressedListener = () -> {
-            if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR)) {
+            if (NotificationIconContainerRefactor.isEnabled()) {
                 mHeadsUpManager.removeNotification(
                         entry.getKey(), /* releaseImmediately= */ true, /* animate= */ false);
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
index 8fee5c0..beeee1b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
@@ -27,7 +27,6 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.widget.ViewClippingUtil;
 import com.android.systemui.flags.FeatureFlagsClassic;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.res.R;
@@ -42,6 +41,7 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationIconInteractor;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor;
 import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
 import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentScope;
@@ -179,7 +179,7 @@
         mHeadsUpManager.addListener(this);
         mView.setOnDrawingRectChangedListener(
                 () -> updateIsolatedIconLocation(true /* requireUpdate */));
-        if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR)) {
+        if (NotificationIconContainerRefactor.isEnabled()) {
             updateIsolatedIconLocation(true);
         }
         mWakeUpCoordinator.addListener(this);
@@ -197,7 +197,7 @@
     protected void onViewDetached() {
         mHeadsUpManager.removeListener(this);
         mView.setOnDrawingRectChangedListener(null);
-        if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR)) {
+        if (NotificationIconContainerRefactor.isEnabled()) {
             mHeadsUpNotificationIconInteractor.setIsolatedIconLocation(null);
         }
         mWakeUpCoordinator.removeListener(this);
@@ -208,7 +208,7 @@
     }
 
     private void updateIsolatedIconLocation(boolean requireStateUpdate) {
-        if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR)) {
+        if (NotificationIconContainerRefactor.isEnabled()) {
             mHeadsUpNotificationIconInteractor
                     .setIsolatedIconLocation(mView.getIconDrawingRect());
         } else {
@@ -250,7 +250,7 @@
                 setShown(true);
                 animateIsolation = !isExpanded();
             }
-            if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR)) {
+            if (NotificationIconContainerRefactor.isEnabled()) {
                 mHeadsUpNotificationIconInteractor.setIsolatedIconNotificationKey(
                         newEntry == null ? null : newEntry.getKey());
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
index f4862c7..3a95e6d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
@@ -34,7 +34,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
 import com.android.systemui.res.R;
-import com.android.systemui.shade.ShadeExpansionStateManager;
+import com.android.systemui.shade.domain.interactor.ShadeInteractor;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener;
@@ -48,6 +48,7 @@
 import com.android.systemui.statusbar.policy.HeadsUpManagerLogger;
 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
 import com.android.systemui.statusbar.policy.OnHeadsUpPhoneListenerChange;
+import com.android.systemui.util.kotlin.JavaAdapter;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -105,7 +106,8 @@
     ///////////////////////////////////////////////////////////////////////////////////////////////
     //  Constructor:
     @Inject
-    public HeadsUpManagerPhone(@NonNull final Context context,
+    public HeadsUpManagerPhone(
+            @NonNull final Context context,
             HeadsUpManagerLogger logger,
             StatusBarStateController statusBarStateController,
             KeyguardBypassController bypassController,
@@ -115,7 +117,8 @@
             @Main Handler handler,
             AccessibilityManagerWrapper accessibilityManagerWrapper,
             UiEventLogger uiEventLogger,
-            ShadeExpansionStateManager shadeExpansionStateManager) {
+            JavaAdapter javaAdapter,
+            ShadeInteractor shadeInteractor) {
         super(context, logger, handler, accessibilityManagerWrapper, uiEventLogger);
         Resources resources = mContext.getResources();
         mExtensionTime = resources.getInteger(R.integer.ambient_notification_extension_time);
@@ -136,8 +139,7 @@
                 updateResources();
             }
         });
-
-        shadeExpansionStateManager.addFullExpansionListener(this::onShadeExpansionFullyChanged);
+        javaAdapter.alwaysCollectFlow(shadeInteractor.isAnyExpanded(), this::onShadeOrQsExpanded);
     }
 
     public void setAnimationStateHandler(AnimationStateHandler handler) {
@@ -230,7 +232,7 @@
         mTrackingHeadsUp = trackingHeadsUp;
     }
 
-    private void onShadeExpansionFullyChanged(Boolean isExpanded) {
+    private void onShadeOrQsExpanded(Boolean isExpanded) {
         if (isExpanded != mIsExpanded) {
             mIsExpanded = isExpanded;
             if (isExpanded) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
index 4284c96c..dd32434 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
@@ -481,7 +481,7 @@
             hostLayout.addView(expected, i);
         }
         hostLayout.setChangingViewPositions(false);
-        hostLayout.setReplacingIcons(null);
+        hostLayout.setReplacingIconsLegacy(null);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.kt
index 0079f7c..2823b28 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.kt
@@ -28,7 +28,7 @@
  */
 interface NotificationIconAreaController {
     /** Called by the Keyguard*ViewController whose view contains the aod icons. */
-    fun setupAodIcons(aodIcons: NotificationIconContainer)
+    fun setupAodIcons(aodIcons: NotificationIconContainer?)
     fun setupShelf(notificationShelfController: NotificationShelfController)
     fun setShelfIcons(icons: NotificationIconContainer)
     fun onDensityOrFontScaleChanged(context: Context)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaControllerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaControllerModule.kt
index d1ddd51..ba69370 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaControllerModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaControllerModule.kt
@@ -15,9 +15,8 @@
  */
 package com.android.systemui.statusbar.phone
 
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconAreaControllerViewBinderWrapperImpl
+import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
 import dagger.Module
 import dagger.Provides
 import javax.inject.Provider
@@ -26,11 +25,10 @@
 object NotificationIconAreaControllerModule {
     @Provides
     fun provideNotificationIconAreaControllerImpl(
-        featureFlags: FeatureFlags,
         legacyProvider: Provider<LegacyNotificationIconAreaControllerImpl>,
         newProvider: Provider<NotificationIconAreaControllerViewBinderWrapperImpl>,
     ): NotificationIconAreaController =
-        if (featureFlags.isEnabled(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR)) {
+        if (NotificationIconContainerRefactor.isEnabled) {
             newProvider.get()
         } else {
             legacyProvider.get()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
index 535f6ac..efb8e2c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -40,10 +40,9 @@
 import com.android.app.animation.Interpolators;
 import com.android.internal.statusbar.StatusBarIcon;
 import com.android.settingslib.Utils;
-import com.android.systemui.flags.Flags;
-import com.android.systemui.flags.RefactorFlag;
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.StatusBarIconView;
+import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor;
 import com.android.systemui.statusbar.notification.stack.AnimationFilter;
 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
 import com.android.systemui.statusbar.notification.stack.ViewState;
@@ -133,9 +132,6 @@
         }
     }.setDuration(CONTENT_FADE_DURATION);
 
-    private final RefactorFlag mIconContainerRefactorFlag =
-            RefactorFlag.forView(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR);
-
     /* Maximum number of icons on AOD when also showing overflow dot. */
     private int mMaxIconsOnAod;
 
@@ -352,7 +348,7 @@
         StatusBarIconView iconView = (StatusBarIconView) child;
         Icon sourceIcon = iconView.getSourceIcon();
         String groupKey = iconView.getNotification().getGroupKey();
-        if (mIconContainerRefactorFlag.isEnabled()) {
+        if (NotificationIconContainerRefactor.isEnabled()) {
             if (mReplacingIcons == null) {
                 return false;
             }
@@ -695,18 +691,18 @@
     }
 
     public void setReplacingIconsLegacy(ArrayMap<String, ArrayList<StatusBarIcon>> replacingIcons) {
-        mIconContainerRefactorFlag.assertInLegacyMode();
+        NotificationIconContainerRefactor.assertInLegacyMode();
         mReplacingIconsLegacy = replacingIcons;
     }
 
     public void setReplacingIcons(ArrayMap<String, StatusBarIcon> replacingIcons) {
-        if (mIconContainerRefactorFlag.isUnexpectedlyInLegacyMode()) return;
+        if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return;
         mReplacingIcons = replacingIcons;
     }
 
     @Deprecated
     public void showIconIsolated(StatusBarIconView icon, boolean animated) {
-        mIconContainerRefactorFlag.assertInLegacyMode();
+        NotificationIconContainerRefactor.assertInLegacyMode();
         if (animated) {
             showIconIsolatedAnimated(icon, null);
         } else {
@@ -716,14 +712,14 @@
 
     public void showIconIsolatedAnimated(StatusBarIconView icon,
             @Nullable Runnable onAnimationEnd) {
-        if (mIconContainerRefactorFlag.isUnexpectedlyInLegacyMode()) return;
+        if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return;
         mIsolatedIconForAnimation = icon != null ? icon : mIsolatedIcon;
         mIsolatedIconAnimationEndRunnable = onAnimationEnd;
         showIconIsolated(icon);
     }
 
     public void showIconIsolated(StatusBarIconView icon) {
-        if (mIconContainerRefactorFlag.isUnexpectedlyInLegacyMode()) return;
+        if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return;
         mIsolatedIcon = icon;
         updateState();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHideIconsForBouncerManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHideIconsForBouncerManager.kt
index 4d9de09..fa6d279 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHideIconsForBouncerManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHideIconsForBouncerManager.kt
@@ -3,12 +3,15 @@
 import android.app.StatusBarManager
 import com.android.systemui.Dumpable
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.shade.ShadeExpansionStateManager
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.window.StatusBarWindowStateController
 import com.android.systemui.util.concurrency.DelayableExecutor
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
 import java.io.PrintWriter
 import javax.inject.Inject
 
@@ -25,14 +28,14 @@
  */
 @SysUISingleton
 class StatusBarHideIconsForBouncerManager @Inject constructor(
-        private val commandQueue: CommandQueue,
-        @Main private val mainExecutor: DelayableExecutor,
-        statusBarWindowStateController: StatusBarWindowStateController,
-        shadeExpansionStateManager: ShadeExpansionStateManager,
-        dumpManager: DumpManager
+    @Application private val scope: CoroutineScope,
+    private val commandQueue: CommandQueue,
+    @Main private val mainExecutor: DelayableExecutor,
+    statusBarWindowStateController: StatusBarWindowStateController,
+    val shadeInteractor: ShadeInteractor,
+    dumpManager: DumpManager
 ) : Dumpable {
     // State variables set by external classes.
-    private var panelExpanded: Boolean = false
     private var isOccluded: Boolean = false
     private var bouncerShowing: Boolean = false
     private var topAppHidesStatusBar: Boolean = false
@@ -49,10 +52,9 @@
         statusBarWindowStateController.addListener {
                 state -> setStatusBarStateAndTriggerUpdate(state)
         }
-        shadeExpansionStateManager.addFullExpansionListener { isExpanded ->
-            if (panelExpanded != isExpanded) {
-                panelExpanded = isExpanded
-                updateHideIconsForBouncer(animate = false)
+        scope.launch {
+            shadeInteractor.isAnyExpanded.collect {
+                updateHideIconsForBouncer(false)
             }
         }
     }
@@ -101,7 +103,7 @@
             topAppHidesStatusBar &&
                     isOccluded &&
                     (statusBarWindowHidden || bouncerShowing)
-        val hideBecauseKeyguard = !panelExpanded && !isOccluded && bouncerShowing
+        val hideBecauseKeyguard = !isShadeOrQsExpanded() && !isOccluded && bouncerShowing
         val shouldHideIconsForBouncer = hideBecauseApp || hideBecauseKeyguard
         if (hideIconsForBouncer != shouldHideIconsForBouncer) {
             hideIconsForBouncer = shouldHideIconsForBouncer
@@ -125,9 +127,13 @@
         }
     }
 
+    private fun isShadeOrQsExpanded(): Boolean {
+        return shadeInteractor.isAnyExpanded.value
+    }
+
     override fun dump(pw: PrintWriter, args: Array<out String>) {
         pw.println("---- State variables set externally ----")
-        pw.println("panelExpanded=$panelExpanded")
+        pw.println("isShadeOrQsExpanded=${isShadeOrQsExpanded()}")
         pw.println("isOccluded=$isOccluded")
         pw.println("bouncerShowing=$bouncerShowing")
         pw.println("topAppHideStatusBar=$topAppHidesStatusBar")
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
index ba73c10..6d8ec44 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
@@ -39,6 +39,7 @@
 import com.android.systemui.scene.domain.interactor.SceneInteractor;
 import com.android.systemui.scene.shared.flag.SceneContainerFlags;
 import com.android.systemui.shade.ShadeExpansionStateManager;
+import com.android.systemui.shade.domain.interactor.ShadeInteractor;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
@@ -86,8 +87,9 @@
             ConfigurationController configurationController,
             HeadsUpManager headsUpManager,
             ShadeExpansionStateManager shadeExpansionStateManager,
+            ShadeInteractor shadeInteractor,
             Provider<SceneInteractor> sceneInteractor,
-            Provider<JavaAdapter> javaAdapter,
+            JavaAdapter javaAdapter,
             SceneContainerFlags sceneContainerFlags,
             UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
             PrimaryBouncerInteractor primaryBouncerInteractor,
@@ -126,12 +128,12 @@
         });
 
         mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
-        shadeExpansionStateManager.addFullExpansionListener(this::onShadeExpansionFullyChanged);
+        javaAdapter.alwaysCollectFlow(shadeInteractor.isAnyExpanded(), this::onShadeOrQsExpanded);
 
         if (sceneContainerFlags.isEnabled()) {
-            javaAdapter.get().alwaysCollectFlow(
+            javaAdapter.alwaysCollectFlow(
                     sceneInteractor.get().isVisible(),
-                    this::onShadeExpansionFullyChanged);
+                    this::onShadeOrQsExpanded);
         }
 
         mPrimaryBouncerInteractor = primaryBouncerInteractor;
@@ -151,7 +153,7 @@
         pw.println(mTouchableRegion);
     }
 
-    private void onShadeExpansionFullyChanged(Boolean isExpanded) {
+    private void onShadeOrQsExpanded(Boolean isExpanded) {
         if (isExpanded != mIsStatusBarExpanded) {
             mIsStatusBarExpanded = isExpanded;
             if (isExpanded) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
index e2a4714..3921e69 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
@@ -44,7 +44,6 @@
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlagsClassic;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.res.R;
 import com.android.systemui.shade.ShadeExpansionStateManager;
@@ -59,12 +58,10 @@
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder;
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarNotificationIconViewStore;
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel;
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel;
-import com.android.systemui.statusbar.phone.DozeParameters;
+import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.NotificationIconContainer;
 import com.android.systemui.statusbar.phone.PhoneStatusBarView;
-import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
 import com.android.systemui.statusbar.phone.StatusBarHideIconsForBouncerManager;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.statusbar.phone.StatusBarIconController.DarkIconManager;
@@ -86,6 +83,8 @@
 import com.android.systemui.util.CarrierConfigTracker.DefaultDataSubscriptionChangedListener;
 import com.android.systemui.util.settings.SecureSettings;
 
+import kotlin.Unit;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -96,8 +95,6 @@
 
 import javax.inject.Inject;
 
-import kotlin.Unit;
-
 /**
  * Contains the collapsed status bar and handles hiding/showing based on disable flags
  * and keyguard state. Also manages lifecycle to make sure the views it contains are being
@@ -155,12 +152,10 @@
     private final DumpManager mDumpManager;
     private final StatusBarWindowStateController mStatusBarWindowStateController;
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-    private final NotificationIconContainerViewModel mStatusBarIconsViewModel;
+    private final NotificationIconContainerStatusBarViewModel mStatusBarIconsViewModel;
     private final ConfigurationState mConfigurationState;
     private final ConfigurationController mConfigurationController;
-    private final DozeParameters mDozeParameters;
-    private final ScreenOffAnimationController mScreenOffAnimationController;
-    private final NotificationIconContainerViewBinder.IconViewStore mStatusBarIconViewStore;
+    private final StatusBarNotificationIconViewStore mStatusBarIconViewStore;
     private final DemoModeController mDemoModeController;
 
     private List<String> mBlockedIcons = new ArrayList<>();
@@ -252,8 +247,6 @@
             NotificationIconContainerStatusBarViewModel statusBarIconsViewModel,
             ConfigurationState configurationState,
             ConfigurationController configurationController,
-            DozeParameters dozeParameters,
-            ScreenOffAnimationController screenOffAnimationController,
             StatusBarNotificationIconViewStore statusBarIconViewStore,
             DemoModeController demoModeController) {
         mStatusBarFragmentComponentFactory = statusBarFragmentComponentFactory;
@@ -283,8 +276,6 @@
         mStatusBarIconsViewModel = statusBarIconsViewModel;
         mConfigurationState = configurationState;
         mConfigurationController = configurationController;
-        mDozeParameters = dozeParameters;
-        mScreenOffAnimationController = screenOffAnimationController;
         mStatusBarIconViewStore = statusBarIconViewStore;
         mDemoModeController = demoModeController;
     }
@@ -317,7 +308,7 @@
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         mStatusBarWindowStateController.addListener(mStatusBarWindowStateListener);
-        if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR)) {
+        if (NotificationIconContainerRefactor.isEnabled()) {
             mDemoModeController.addCallback(mDemoModeCallback);
         }
     }
@@ -326,7 +317,7 @@
     public void onDestroy() {
         super.onDestroy();
         mStatusBarWindowStateController.removeListener(mStatusBarWindowStateListener);
-        if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR)) {
+        if (NotificationIconContainerRefactor.isEnabled()) {
             mDemoModeController.removeCallback(mDemoModeCallback);
         }
     }
@@ -469,7 +460,7 @@
     /** Initializes views related to the notification icon area. */
     public void initNotificationIconArea() {
         ViewGroup notificationIconArea = mStatusBar.requireViewById(R.id.notification_icon_area);
-        if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR)) {
+        if (NotificationIconContainerRefactor.isEnabled()) {
             mNotificationIconAreaInner =
                 LayoutInflater.from(getContext())
                         .inflate(R.layout.notification_icon_area, notificationIconArea, true);
@@ -480,9 +471,6 @@
                     mStatusBarIconsViewModel,
                     mConfigurationState,
                     mConfigurationController,
-                    mDozeParameters,
-                    mFeatureFlags,
-                    mScreenOffAnimationController,
                     mStatusBarIconViewStore);
         } else {
             mNotificationIconAreaInner =
@@ -606,7 +594,7 @@
 
         // Hide notifications if the disable flag is set or we have an ongoing call.
         if (disableNotifications || hasOngoingCall) {
-            hideNotificationIconArea(animate);
+            hideNotificationIconArea(animate && !hasOngoingCall);
         } else {
             showNotificationIconArea(animate);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/VariableDateViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/VariableDateViewController.kt
index eaae0f0..fa6ea4c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/VariableDateViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/VariableDateViewController.kt
@@ -30,10 +30,13 @@
 import android.util.Log
 import android.view.View.MeasureSpec
 import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
 import com.android.systemui.Dependency
 import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.shade.ShadeExpansionStateManager
+import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.shade.ShadeLogger
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.util.ViewController
 import com.android.systemui.util.time.SystemClock
 import java.text.FieldPosition
@@ -83,7 +86,7 @@
 class VariableDateViewController(
     private val systemClock: SystemClock,
     private val broadcastDispatcher: BroadcastDispatcher,
-    private val shadeExpansionStateManager: ShadeExpansionStateManager,
+    private val shadeInteractor: ShadeInteractor,
     private val shadeLogger: ShadeLogger,
     private val timeTickHandler: Handler,
     view: VariableDateView
@@ -174,8 +177,11 @@
 
         broadcastDispatcher.registerReceiver(intentReceiver, filter,
                 HandlerExecutor(timeTickHandler), UserHandle.SYSTEM)
-
-        shadeExpansionStateManager.addQsExpansionFractionListener(::onQsExpansionFractionChanged)
+        mView.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                shadeInteractor.qsExpansion.collect(::onQsExpansionFractionChanged)
+            }
+        }
         post(::updateClock)
         mView.onAttach(onMeasureListener)
     }
@@ -183,7 +189,6 @@
     override fun onViewDetached() {
         dateFormat = null
         mView.onAttach(null)
-        shadeExpansionStateManager.removeQsExpansionFractionListener(::onQsExpansionFractionChanged)
         broadcastDispatcher.unregisterReceiver(intentReceiver)
     }
 
@@ -237,18 +242,18 @@
     class Factory @Inject constructor(
         private val systemClock: SystemClock,
         private val broadcastDispatcher: BroadcastDispatcher,
-        private val shadeExpansionStateManager: ShadeExpansionStateManager,
+        private val shadeInteractor: ShadeInteractor,
         private val shadeLogger: ShadeLogger,
         @Named(Dependency.TIME_TICK_HANDLER_NAME) private val handler: Handler
     ) {
         fun create(view: VariableDateView): VariableDateViewController {
             return VariableDateViewController(
-                    systemClock,
-                    broadcastDispatcher,
-                    shadeExpansionStateManager,
-                    shadeLogger,
-                    handler,
-                    view
+                systemClock,
+                broadcastDispatcher,
+                shadeInteractor,
+                shadeLogger,
+                handler,
+                view
             )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/util/view/DisposableHandleExt.kt b/packages/SystemUI/src/com/android/systemui/util/view/DisposableHandleExt.kt
new file mode 100644
index 0000000..d3653b4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/view/DisposableHandleExt.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.view
+
+import android.view.View
+import com.android.systemui.util.kotlin.awaitCancellationThenDispose
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collectLatest
+
+/**
+ * Use the [bind] method to bind the view every time this flow emits, and suspend to await for more
+ * updates. New emissions lead to the previous binding call being cancelled if not completed.
+ * Dispose of the [DisposableHandle] returned by [bind] when done.
+ */
+suspend fun <T : View> Flow<T>.bindLatest(bind: (T) -> DisposableHandle?) {
+    this.collectLatest { view ->
+        val disposableHandle = bind(view)
+        disposableHandle?.awaitCancellationThenDispose()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/view/LayoutInflaterExt.kt b/packages/SystemUI/src/com/android/systemui/util/view/LayoutInflaterExt.kt
deleted file mode 100644
index 6d45d23..0000000
--- a/packages/SystemUI/src/com/android/systemui/util/view/LayoutInflaterExt.kt
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.util.view
-
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.util.kotlin.awaitCancellationThenDispose
-import com.android.systemui.util.kotlin.stateFlow
-import kotlinx.coroutines.DisposableHandle
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.collectLatest
-
-/**
- * Perform an inflation right away, then re-inflate whenever the [flow] emits, and call [onInflate]
- * on the resulting view each time. Dispose of the [DisposableHandle] returned by [onInflate] when
- * done.
- *
- * This never completes unless cancelled, it just suspends and waits for updates.
- *
- * For parameters [resource], [root] and [attachToRoot], see [LayoutInflater.inflate].
- *
- * An example use-case of this is when a view needs to be re-inflated whenever a configuration
- * change occurs, which would require the ViewBinder to then re-bind the new view. For example, the
- * code in the parent view's binder would look like:
- * ```
- * parentView.repeatWhenAttached {
- *     LayoutInflater.from(parentView.context)
- *         .reinflateOnChange(
- *             R.layout.my_layout,
- *             parentView,
- *             attachToRoot = false,
- *             coroutineScope = lifecycleScope,
- *             configurationController.onThemeChanged,
- *             ),
- *     ) { view ->
- *         ChildViewBinder.bind(view as ChildView, childViewModel)
- *     }
- * }
- * ```
- *
- * In turn, the bind method (passed through [onInflate]) uses [repeatWhenAttached], which returns a
- * [DisposableHandle].
- */
-suspend fun LayoutInflater.reinflateAndBindLatest(
-    resource: Int,
-    root: ViewGroup?,
-    attachToRoot: Boolean,
-    flow: Flow<Unit>,
-    onInflate: (View) -> DisposableHandle?,
-) = coroutineScope {
-    val viewFlow: Flow<View> = stateFlow(flow) { inflate(resource, root, attachToRoot) }
-    viewFlow.bindLatest(onInflate)
-}
-
-/**
- * Use the [bind] method to bind the view every time this flow emits, and suspend to await for more
- * updates. New emissions lead to the previous binding call being cancelled if not completed.
- * Dispose of the [DisposableHandle] returned by [bind] when done.
- */
-suspend fun Flow<View>.bindLatest(bind: (View) -> DisposableHandle?) {
-    this.collectLatest { view ->
-        val disposableHandle = bind(view)
-        disposableHandle?.awaitCancellationThenDispose()
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/TestMocksModule.kt b/packages/SystemUI/tests/src/com/android/TestMocksModule.kt
index f49ba64..0cb913b 100644
--- a/packages/SystemUI/tests/src/com/android/TestMocksModule.kt
+++ b/packages/SystemUI/tests/src/com/android/TestMocksModule.kt
@@ -19,6 +19,7 @@
 import android.app.admin.DevicePolicyManager
 import android.os.UserManager
 import android.util.DisplayMetrics
+import android.view.LayoutInflater
 import com.android.internal.logging.MetricsLogger
 import com.android.keyguard.KeyguardSecurityModel
 import com.android.keyguard.KeyguardUpdateMonitor
@@ -37,6 +38,7 @@
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.DarkIconDispatcher
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.LockscreenShadeTransitionController
 import com.android.systemui.statusbar.NotificationListener
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.NotificationMediaManager
@@ -75,6 +77,9 @@
     @get:Provides val keyguardBypassController: KeyguardBypassController = mock(),
     @get:Provides val keyguardSecurityModel: KeyguardSecurityModel = mock(),
     @get:Provides val keyguardUpdateMonitor: KeyguardUpdateMonitor = mock(),
+    @get:Provides val layoutInflater: LayoutInflater = mock(),
+    @get:Provides
+    val lockscreenShadeTransitionController: LockscreenShadeTransitionController = mock(),
     @get:Provides val mediaHierarchyManager: MediaHierarchyManager = mock(),
     @get:Provides val notifCollection: NotifCollection = mock(),
     @get:Provides val notificationListener: NotificationListener = mock(),
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java
index 5273e0e5..ac40ba6 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java
@@ -21,6 +21,7 @@
 import static com.android.systemui.flags.Flags.FACE_AUTH_REFACTOR;
 import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED;
 import static com.android.systemui.flags.Flags.MIGRATE_KEYGUARD_STATUS_VIEW;
+import static com.android.systemui.flags.SetFlagsRuleExtensionsKt.setFlagDefault;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.atLeast;
@@ -35,8 +36,8 @@
 import android.widget.LinearLayout;
 import android.widget.RelativeLayout;
 
-import com.android.systemui.res.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.common.ui.ConfigurationState;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
@@ -50,12 +51,19 @@
 import com.android.systemui.plugins.ClockFaceEvents;
 import com.android.systemui.plugins.ClockTickRate;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.res.R;
 import com.android.systemui.shared.clocks.AnimatableClockView;
 import com.android.systemui.shared.clocks.ClockRegistry;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController;
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore;
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel;
+import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor;
+import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.NotificationIconContainer;
+import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
+import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.settings.SecureSettings;
 import com.android.systemui.util.time.FakeSystemClock;
@@ -136,6 +144,7 @@
     public void setup() {
         MockitoAnnotations.initMocks(this);
 
+        setFlagDefault(mSetFlagsRule, NotificationIconContainerRefactor.FLAG_NAME);
         mFakeDateView.setTag(R.id.tag_smartspace_view, new Object());
         mFakeWeatherView.setTag(R.id.tag_smartspace_view, new Object());
         mFakeSmartspaceView.setTag(R.id.tag_smartspace_view, new Object());
@@ -176,12 +185,18 @@
                 mKeyguardSliceViewController,
                 mNotificationIconAreaController,
                 mSmartspaceController,
+                mock(ConfigurationController.class),
+                mock(ScreenOffAnimationController.class),
                 mKeyguardUnlockAnimationController,
                 mSecureSettings,
                 mExecutor,
                 mDumpManager,
                 mClockEventController,
                 mLogBuffer,
+                mock(NotificationIconContainerAlwaysOnDisplayViewModel.class),
+                mock(ConfigurationState.class),
+                mock(DozeParameters.class),
+                mock(AlwaysOnDisplayNotificationIconViewStore.class),
                 KeyguardInteractorFactory.create(mFakeFeatureFlags).getKeyguardInteractor(),
                 mFakeFeatureFlags
         );
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MockMagnificationAnimationCallback.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MockMagnificationAnimationCallback.java
deleted file mode 100644
index 89389b0..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MockMagnificationAnimationCallback.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.accessibility;
-
-import android.os.RemoteException;
-import android.view.accessibility.IRemoteMagnificationAnimationCallback;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.atomic.AtomicInteger;
-
-public class MockMagnificationAnimationCallback extends IRemoteMagnificationAnimationCallback.Stub {
-
-    private final CountDownLatch mCountDownLatch;
-    private final AtomicInteger mSuccessCount;
-    private final AtomicInteger mFailedCount;
-
-    MockMagnificationAnimationCallback(CountDownLatch countDownLatch) {
-        mCountDownLatch = countDownLatch;
-        mSuccessCount = new AtomicInteger();
-        mFailedCount = new AtomicInteger();
-    }
-
-    public int getSuccessCount() {
-        return mSuccessCount.get();
-    }
-
-    public int getFailedCount() {
-        return mFailedCount.get();
-    }
-
-    @Override
-    public void onResult(boolean success) throws RemoteException {
-        if (success) {
-            mSuccessCount.getAndIncrement();
-        } else {
-            mFailedCount.getAndIncrement();
-        }
-        // It should be put at the last line to avoid making CountDownLatch#await passed without
-        // updating values.
-        mCountDownLatch.countDown();
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
index f15164e..284c273 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
@@ -35,7 +35,6 @@
 import android.os.Handler;
 import android.os.RemoteException;
 import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper.RunWithLooper;
 import android.view.SurfaceControl;
 import android.view.View;
 import android.view.WindowManager;
@@ -68,7 +67,6 @@
 
 @LargeTest
 @RunWith(AndroidTestingRunner.class)
-@RunWithLooper(setAsMainLooper = true)
 public class WindowMagnificationAnimationControllerTest extends SysuiTestCase {
 
     @Rule
@@ -135,7 +133,9 @@
 
     @After
     public void tearDown() throws Exception {
-        mController.deleteWindowMagnification();
+        mInstrumentation.runOnMainSync(() -> {
+            mController.deleteWindowMagnification();
+        });
     }
 
     @Test
@@ -170,8 +170,7 @@
         final float targetCenterX = DEFAULT_CENTER_X + 100;
         final float targetCenterY = DEFAULT_CENTER_Y + 100;
 
-        mWindowMagnificationAnimationController.enableWindowMagnification(targetScale,
-                targetCenterX, targetCenterY, null);
+        enableWindowMagnificationWithoutAnimation(targetScale, targetCenterX, targetCenterY);
 
         verifyFinalSpec(targetScale, targetCenterX, targetCenterY);
     }
@@ -179,10 +178,11 @@
     @Test
     public void enableWindowMagnificationWithScaleOne_disabled_NoAnimationAndInvokeCallback()
             throws RemoteException {
-        mWindowMagnificationAnimationController.enableWindowMagnification(1,
+        enableWindowMagnificationAndWaitAnimating(
+                mWaitAnimationDuration, /* targetScale= */ 1.0f,
                 DEFAULT_CENTER_X, DEFAULT_CENTER_Y, mAnimationCallback);
 
-        verify(mSpyController).enableWindowMagnificationInternal(1, DEFAULT_CENTER_X,
+        verify(mSpyController).enableWindowMagnificationInternal(1.0f, DEFAULT_CENTER_X,
                 DEFAULT_CENTER_Y, 0f, 0f);
         verify(mAnimationCallback).onResult(true);
     }
@@ -196,13 +196,15 @@
         final float targetCenterX = DEFAULT_CENTER_X + 100;
         final float targetCenterY = DEFAULT_CENTER_Y + 100;
 
-        Mockito.reset(mSpyController);
-        mWindowMagnificationAnimationController.enableWindowMagnification(targetScale,
-                targetCenterX, targetCenterY, mAnimationCallback2);
-        mCurrentScale.set(mController.getScale());
-        mCurrentCenterX.set(mController.getCenterX());
-        mCurrentCenterY.set(mController.getCenterY());
-        advanceTimeBy(mWaitAnimationDuration);
+        resetMockObjects();
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationAnimationController.enableWindowMagnification(targetScale,
+                    targetCenterX, targetCenterY, mAnimationCallback2);
+            mCurrentScale.set(mController.getScale());
+            mCurrentCenterX.set(mController.getCenterX());
+            mCurrentCenterY.set(mController.getCenterY());
+            advanceTimeBy(mWaitAnimationDuration);
+        });
 
         verify(mSpyController, atLeast(2)).enableWindowMagnificationInternal(
                 mScaleCaptor.capture(),
@@ -224,9 +226,8 @@
         enableWindowMagnificationAndWaitAnimating(mWaitPartialAnimationDuration,
                 mAnimationCallback);
 
-        mWindowMagnificationAnimationController.enableWindowMagnification(Float.NaN,
+        enableWindowMagnificationAndWaitAnimating(mWaitAnimationDuration, Float.NaN,
                 Float.NaN, Float.NaN, mAnimationCallback2);
-        advanceTimeBy(mWaitAnimationDuration);
 
         // The callback in 2nd enableWindowMagnification will return true
         verify(mAnimationCallback2).onResult(true);
@@ -245,12 +246,14 @@
         final float targetCenterY = DEFAULT_CENTER_Y + 100;
 
         Mockito.reset(mSpyController);
-        mWindowMagnificationAnimationController.enableWindowMagnification(targetScale,
-                targetCenterX, targetCenterY, mAnimationCallback);
-        mCurrentScale.set(mController.getScale());
-        mCurrentCenterX.set(mController.getCenterX());
-        mCurrentCenterY.set(mController.getCenterY());
-        advanceTimeBy(mWaitAnimationDuration);
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationAnimationController.enableWindowMagnification(targetScale,
+                    targetCenterX, targetCenterY, mAnimationCallback);
+            mCurrentScale.set(mController.getScale());
+            mCurrentCenterX.set(mController.getCenterX());
+            mCurrentCenterY.set(mController.getCenterY());
+            advanceTimeBy(mWaitAnimationDuration);
+        });
 
         verify(mSpyController, atLeast(2)).enableWindowMagnificationInternal(
                 mScaleCaptor.capture(),
@@ -279,12 +282,14 @@
         final float targetCenterY = DEFAULT_CENTER_Y + 100;
 
         Mockito.reset(mSpyController);
-        mWindowMagnificationAnimationController.enableWindowMagnification(targetScale,
-                targetCenterX, targetCenterY, mAnimationCallback);
-        mCurrentScale.set(mController.getScale());
-        mCurrentCenterX.set(mController.getCenterX());
-        mCurrentCenterY.set(mController.getCenterY());
-        advanceTimeBy(mWaitAnimationDuration);
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationAnimationController.enableWindowMagnification(targetScale,
+                    targetCenterX, targetCenterY, mAnimationCallback);
+            mCurrentScale.set(mController.getScale());
+            mCurrentCenterX.set(mController.getCenterX());
+            mCurrentCenterY.set(mController.getCenterY());
+            advanceTimeBy(mWaitAnimationDuration);
+        });
 
         verify(mSpyController, atLeast(2)).enableWindowMagnificationInternal(
                 mScaleCaptor.capture(),
@@ -314,8 +319,7 @@
         final float targetCenterY = DEFAULT_CENTER_Y + 100;
 
         Mockito.reset(mSpyController);
-        mWindowMagnificationAnimationController.enableWindowMagnification(targetScale,
-                targetCenterX, targetCenterY, null);
+        enableWindowMagnificationWithoutAnimation(targetScale, targetCenterX, targetCenterY);
 
         verifyFinalSpec(Float.NaN, Float.NaN, Float.NaN);
         assertEquals(WindowMagnificationAnimationController.STATE_DISABLED,
@@ -333,8 +337,7 @@
         final float targetCenterY = DEFAULT_CENTER_Y + 100;
 
         Mockito.reset(mSpyController);
-        mWindowMagnificationAnimationController.enableWindowMagnification(targetScale,
-                targetCenterX, targetCenterY, null);
+        enableWindowMagnificationWithoutAnimation(targetScale, targetCenterX, targetCenterY);
 
         verifyFinalSpec(targetScale, targetCenterX, targetCenterY);
         verify(mAnimationCallback).onResult(false);
@@ -347,9 +350,8 @@
                 mAnimationCallback);
 
         Mockito.reset(mSpyController);
-        mWindowMagnificationAnimationController.enableWindowMagnification(Float.NaN,
-                Float.NaN, Float.NaN, mAnimationCallback2);
-        advanceTimeBy(mWaitAnimationDuration);
+        enableWindowMagnificationAndWaitAnimating(
+                mWaitAnimationDuration, Float.NaN, Float.NaN, Float.NaN, mAnimationCallback2);
 
         verify(mSpyController, never()).enableWindowMagnificationInternal(anyFloat(), anyFloat(),
                 anyFloat());
@@ -368,20 +370,25 @@
         final float targetCenterY = DEFAULT_CENTER_Y + 100;
 
         Mockito.reset(mSpyController);
-        mWindowMagnificationAnimationController.enableWindowMagnification(targetScale,
-                targetCenterX, targetCenterY, mAnimationCallback2);
-        mCurrentScale.set(mController.getScale());
-        mCurrentCenterX.set(mController.getCenterX());
-        mCurrentCenterY.set(mController.getCenterY());
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationAnimationController.enableWindowMagnification(targetScale,
+                    targetCenterX, targetCenterY, mAnimationCallback2);
+            mCurrentScale.set(mController.getScale());
+            mCurrentCenterX.set(mController.getCenterX());
+            mCurrentCenterY.set(mController.getCenterY());
+        });
 
         // Current spec shouldn't match given spec.
         verify(mAnimationCallback2, never()).onResult(anyBoolean());
         verify(mAnimationCallback).onResult(false);
-        // ValueAnimator.reverse() could not work correctly with the AnimatorTestRule since it is
-        // using SystemClock in reverse() (b/305731398). Therefore, we call end() on the animator
-        // directly to verify the result of animation is correct instead of querying the animation
-        // frame at a specific timing.
-        mValueAnimator.end();
+
+        mInstrumentation.runOnMainSync(() -> {
+            // ValueAnimator.reverse() could not work correctly with the AnimatorTestRule since it
+            // is using SystemClock in reverse() (b/305731398). Therefore, we call end() on the
+            // animator directly to verify the result of animation is correct instead of querying
+            // the animation frame at a specific timing.
+            mValueAnimator.end();
+        });
 
         verify(mSpyController).enableWindowMagnificationInternal(
                 mScaleCaptor.capture(),
@@ -410,8 +417,7 @@
         final float targetCenterY = DEFAULT_CENTER_Y + 100;
 
         Mockito.reset(mSpyController);
-        mWindowMagnificationAnimationController.enableWindowMagnification(targetScale,
-                targetCenterX, targetCenterY, null);
+        enableWindowMagnificationWithoutAnimation(targetScale, targetCenterX, targetCenterY);
 
         verify(mAnimationCallback).onResult(false);
         verifyFinalSpec(targetScale, targetCenterX, targetCenterY);
@@ -425,9 +431,8 @@
                 mAnimationCallback);
 
         Mockito.reset(mSpyController);
-        mWindowMagnificationAnimationController.enableWindowMagnification(Float.NaN,
+        enableWindowMagnificationAndWaitAnimating(mWaitAnimationDuration, Float.NaN,
                 Float.NaN, Float.NaN, mAnimationCallback2);
-        advanceTimeBy(mWaitAnimationDuration);
 
         verify(mSpyController, never()).enableWindowMagnificationInternal(anyFloat(), anyFloat(),
                 anyFloat());
@@ -445,12 +450,14 @@
         final float targetCenterY = DEFAULT_CENTER_Y + 100;
 
         Mockito.reset(mSpyController);
-        mWindowMagnificationAnimationController.enableWindowMagnification(targetScale,
-                targetCenterX, targetCenterY, mAnimationCallback2);
-        mCurrentScale.set(mController.getScale());
-        mCurrentCenterX.set(mController.getCenterX());
-        mCurrentCenterY.set(mController.getCenterY());
-        advanceTimeBy(mWaitAnimationDuration);
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationAnimationController.enableWindowMagnification(targetScale,
+                    targetCenterX, targetCenterY, mAnimationCallback2);
+            mCurrentScale.set(mController.getScale());
+            mCurrentCenterX.set(mController.getCenterX());
+            mCurrentCenterY.set(mController.getCenterY());
+            advanceTimeBy(mWaitAnimationDuration);
+        });
 
         verify(mSpyController, atLeast(2)).enableWindowMagnificationInternal(
                 mScaleCaptor.capture(),
@@ -471,25 +478,26 @@
         final Rect windowBounds = new Rect(mWindowManager.getCurrentWindowMetrics().getBounds());
 
         Mockito.reset(mSpyController);
-        mWindowMagnificationAnimationController.enableWindowMagnification(DEFAULT_SCALE,
-                windowBounds.exactCenterX(), windowBounds.exactCenterY(),
-                offsetRatio, offsetRatio, mAnimationCallback);
-        advanceTimeBy(mWaitAnimationDuration);
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationAnimationController.enableWindowMagnification(DEFAULT_SCALE,
+                    windowBounds.exactCenterX(), windowBounds.exactCenterY(),
+                    offsetRatio, offsetRatio, mAnimationCallback);
+            advanceTimeBy(mWaitAnimationDuration);
+        });
 
-        // We delay the time of verifying to wait for the measurement and layout of the view
-        mHandler.postDelayed(() -> {
-            final View attachedView = mWindowManager.getAttachedView();
-            assertNotNull(attachedView);
-            final Rect mirrorViewBound = new Rect();
-            final View mirrorView = attachedView.findViewById(R.id.surface_view);
-            assertNotNull(mirrorView);
-            mirrorView.getBoundsOnScreen(mirrorViewBound);
+        // Wait for Rects update
+        waitForIdleSync();
+        final View attachedView = mWindowManager.getAttachedView();
+        assertNotNull(attachedView);
+        final Rect mirrorViewBound = new Rect();
+        final View mirrorView = attachedView.findViewById(R.id.surface_view);
+        assertNotNull(mirrorView);
+        mirrorView.getBoundsOnScreen(mirrorViewBound);
 
-            assertEquals((int) (offsetRatio * mirrorViewBound.width() / 2),
-                    (int) (mirrorViewBound.exactCenterX() - windowBounds.exactCenterX()));
-            assertEquals((int) (offsetRatio * mirrorViewBound.height() / 2),
-                    (int) (mirrorViewBound.exactCenterY() - windowBounds.exactCenterY()));
-        }, 100);
+        assertEquals((int) (offsetRatio * mirrorViewBound.width() / 2),
+                (int) (mirrorViewBound.exactCenterX() - windowBounds.exactCenterX()));
+        assertEquals((int) (offsetRatio * mirrorViewBound.height() / 2),
+                (int) (mirrorViewBound.exactCenterY() - windowBounds.exactCenterY()));
     }
 
     @Test
@@ -498,9 +506,11 @@
         final float targetCenterY = DEFAULT_CENTER_Y + 100;
         enableWindowMagnificationWithoutAnimation();
 
-        mWindowMagnificationAnimationController.moveWindowMagnifierToPosition(
-                targetCenterX, targetCenterY, mAnimationCallback);
-        advanceTimeBy(mWaitAnimationDuration);
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationAnimationController.moveWindowMagnifierToPosition(
+                    targetCenterX, targetCenterY, mAnimationCallback);
+            advanceTimeBy(mWaitAnimationDuration);
+        });
 
         verify(mAnimationCallback).onResult(true);
         verify(mAnimationCallback, never()).onResult(false);
@@ -512,15 +522,17 @@
             throws RemoteException {
         enableWindowMagnificationWithoutAnimation();
 
-        mWindowMagnificationAnimationController.moveWindowMagnifierToPosition(
-                DEFAULT_CENTER_X + 10, DEFAULT_CENTER_Y + 10, mAnimationCallback);
-        mWindowMagnificationAnimationController.moveWindowMagnifierToPosition(
-                DEFAULT_CENTER_X + 20, DEFAULT_CENTER_Y + 20, mAnimationCallback);
-        mWindowMagnificationAnimationController.moveWindowMagnifierToPosition(
-                DEFAULT_CENTER_X + 30, DEFAULT_CENTER_Y + 30, mAnimationCallback);
-        mWindowMagnificationAnimationController.moveWindowMagnifierToPosition(
-                DEFAULT_CENTER_X + 40, DEFAULT_CENTER_Y + 40, mAnimationCallback2);
-        advanceTimeBy(mWaitAnimationDuration);
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationAnimationController.moveWindowMagnifierToPosition(
+                    DEFAULT_CENTER_X + 10, DEFAULT_CENTER_Y + 10, mAnimationCallback);
+            mWindowMagnificationAnimationController.moveWindowMagnifierToPosition(
+                    DEFAULT_CENTER_X + 20, DEFAULT_CENTER_Y + 20, mAnimationCallback);
+            mWindowMagnificationAnimationController.moveWindowMagnifierToPosition(
+                    DEFAULT_CENTER_X + 30, DEFAULT_CENTER_Y + 30, mAnimationCallback);
+            mWindowMagnificationAnimationController.moveWindowMagnifierToPosition(
+                    DEFAULT_CENTER_X + 40, DEFAULT_CENTER_Y + 40, mAnimationCallback2);
+            advanceTimeBy(mWaitAnimationDuration);
+        });
 
         // only the last one callback will return true
         verify(mAnimationCallback2).onResult(true);
@@ -538,9 +550,11 @@
         enableWindowMagnificationAndWaitAnimating(mWaitPartialAnimationDuration,
                 mAnimationCallback);
 
-        mWindowMagnificationAnimationController.moveWindowMagnifierToPosition(
-                targetCenterX, targetCenterY, mAnimationCallback2);
-        advanceTimeBy(mWaitAnimationDuration);
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationAnimationController.moveWindowMagnifierToPosition(
+                    targetCenterX, targetCenterY, mAnimationCallback2);
+            advanceTimeBy(mWaitAnimationDuration);
+        });
 
         // The callback in moveWindowMagnifierToPosition will return true
         verify(mAnimationCallback2).onResult(true);
@@ -556,9 +570,11 @@
         enableWindowMagnificationAndWaitAnimating(mWaitPartialAnimationDuration,
                 mAnimationCallback);
 
-        mWindowMagnificationAnimationController.moveWindowMagnifierToPosition(
-                Float.NaN, Float.NaN, mAnimationCallback2);
-        advanceTimeBy(mWaitAnimationDuration);
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationAnimationController.moveWindowMagnifierToPosition(
+                    Float.NaN, Float.NaN, mAnimationCallback2);
+            advanceTimeBy(mWaitAnimationDuration);
+        });
 
         // The callback in moveWindowMagnifierToPosition will return true
         verify(mAnimationCallback2).onResult(true);
@@ -584,6 +600,7 @@
             throws RemoteException {
         enableWindowMagnificationWithoutAnimation();
 
+        resetMockObjects();
         deleteWindowMagnificationAndWaitAnimating(mWaitAnimationDuration, mAnimationCallback);
 
         verify(mSpyController, atLeast(2)).enableWindowMagnificationInternal(
@@ -625,16 +642,18 @@
                 mAnimationCallback);
 
         Mockito.reset(mSpyController);
-        mWindowMagnificationAnimationController.deleteWindowMagnification(
-                mAnimationCallback2);
-        mCurrentScale.set(mController.getScale());
-        mCurrentCenterX.set(mController.getCenterX());
-        mCurrentCenterY.set(mController.getCenterY());
-        // ValueAnimator.reverse() could not work correctly with the AnimatorTestRule since it is
-        // using SystemClock in reverse() (b/305731398). Therefore, we call end() on the animator
-        // directly to verify the result of animation is correct instead of querying the animation
-        // frame at a specific timing.
-        mValueAnimator.end();
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationAnimationController.deleteWindowMagnification(
+                    mAnimationCallback2);
+            mCurrentScale.set(mController.getScale());
+            mCurrentCenterX.set(mController.getCenterX());
+            mCurrentCenterY.set(mController.getCenterY());
+            // ValueAnimator.reverse() could not work correctly with the AnimatorTestRule since it
+            // is using SystemClock in reverse() (b/305731398). Therefore, we call end() on the
+            // animator directly to verify the result of animation is correct instead of querying
+            // the animation frame at a specific timing.
+            mValueAnimator.end();
+        });
 
         verify(mSpyController).enableWindowMagnificationInternal(
                 mScaleCaptor.capture(),
@@ -661,7 +680,7 @@
                 mAnimationCallback);
 
         Mockito.reset(mSpyController);
-        mWindowMagnificationAnimationController.deleteWindowMagnification(null);
+        deleteWindowMagnificationWithoutAnimation();
 
         verifyFinalSpec(Float.NaN, Float.NaN, Float.NaN);
         verify(mAnimationCallback).onResult(false);
@@ -673,6 +692,7 @@
         deleteWindowMagnificationAndWaitAnimating(mWaitPartialAnimationDuration,
                 mAnimationCallback);
 
+        resetMockObjects();
         deleteWindowMagnificationAndWaitAnimating(mWaitAnimationDuration, mAnimationCallback2);
 
         verify(mSpyController).enableWindowMagnificationInternal(
@@ -710,8 +730,7 @@
         final float offsetY =
                 (float) Math.ceil(offsetX * WindowMagnificationController.HORIZONTAL_LOCK_BASE)
                         + 1.0f;
-
-        mController.moveWindowMagnifier(offsetX, offsetY);
+        mInstrumentation.runOnMainSync(()-> mController.moveWindowMagnifier(offsetX, offsetY));
 
         verify(mSpyController).moveWindowMagnifier(offsetX, offsetY);
         verifyFinalSpec(DEFAULT_SCALE, DEFAULT_CENTER_X, DEFAULT_CENTER_Y + offsetY);
@@ -726,8 +745,8 @@
         final float offsetY =
                 (float) Math.floor(offsetX * WindowMagnificationController.HORIZONTAL_LOCK_BASE)
                         - 1.0f;
-
-        mController.moveWindowMagnifier(offsetX, offsetY);
+        mInstrumentation.runOnMainSync(() ->
+                mController.moveWindowMagnifier(offsetX, offsetY));
 
         verify(mSpyController).moveWindowMagnifier(offsetX, offsetY);
         verifyFinalSpec(DEFAULT_SCALE, DEFAULT_CENTER_X + offsetX, DEFAULT_CENTER_Y);
@@ -742,8 +761,10 @@
                 (float) Math.ceil(offsetX * WindowMagnificationController.HORIZONTAL_LOCK_BASE);
         // while diagonal scrolling enabled,
         //  should move with both offsetX and offsetY without regrading offsetY/offsetX
-        mController.setDiagonalScrolling(true);
-        mController.moveWindowMagnifier(offsetX, offsetY);
+        mInstrumentation.runOnMainSync(() -> {
+            mController.setDiagonalScrolling(true);
+            mController.moveWindowMagnifier(offsetX, offsetY);
+        });
 
         verify(mSpyController).moveWindowMagnifier(offsetX, offsetY);
         verifyFinalSpec(DEFAULT_SCALE, DEFAULT_CENTER_X + offsetX, DEFAULT_CENTER_Y + offsetY);
@@ -755,9 +776,11 @@
         final float targetCenterY = DEFAULT_CENTER_Y + 100;
         enableWindowMagnificationWithoutAnimation();
 
-        mController.moveWindowMagnifierToPosition(targetCenterX, targetCenterY,
-                mAnimationCallback);
-        advanceTimeBy(mWaitAnimationDuration);
+        mInstrumentation.runOnMainSync(() -> {
+            mController.moveWindowMagnifierToPosition(targetCenterX, targetCenterY,
+                    mAnimationCallback);
+            advanceTimeBy(mWaitAnimationDuration);
+        });
 
         verifyFinalSpec(DEFAULT_SCALE, targetCenterX, targetCenterY);
     }
@@ -774,24 +797,49 @@
     }
 
     private void enableWindowMagnificationWithoutAnimation() {
-        Mockito.reset(mSpyController);
-        mWindowMagnificationAnimationController.enableWindowMagnification(DEFAULT_SCALE,
-                DEFAULT_CENTER_X, DEFAULT_CENTER_Y, null);
+        enableWindowMagnificationWithoutAnimation(
+                DEFAULT_SCALE, DEFAULT_CENTER_X, DEFAULT_CENTER_Y);
+    }
+
+    private void enableWindowMagnificationWithoutAnimation(
+            float targetScale, float targetCenterX, float targetCenterY) {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationAnimationController.enableWindowMagnification(
+                    targetScale, targetCenterX, targetCenterY, null);
+        });
     }
 
     private void enableWindowMagnificationAndWaitAnimating(long duration,
             @Nullable IRemoteMagnificationAnimationCallback callback) {
-        Mockito.reset(mSpyController);
-        mWindowMagnificationAnimationController.enableWindowMagnification(DEFAULT_SCALE,
-                DEFAULT_CENTER_X, DEFAULT_CENTER_Y, callback);
-        advanceTimeBy(duration);
+        enableWindowMagnificationAndWaitAnimating(
+                duration, DEFAULT_SCALE, DEFAULT_CENTER_X, DEFAULT_CENTER_Y, callback);
+    }
+
+    private void enableWindowMagnificationAndWaitAnimating(
+            long duration,
+            float targetScale,
+            float targetCenterX,
+            float targetCenterY,
+            @Nullable IRemoteMagnificationAnimationCallback callback) {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationAnimationController.enableWindowMagnification(
+                    targetScale, targetCenterX, targetCenterY, callback);
+            advanceTimeBy(duration);
+        });
+    }
+
+    private void deleteWindowMagnificationWithoutAnimation() {
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationAnimationController.deleteWindowMagnification(null);
+        });
     }
 
     private void deleteWindowMagnificationAndWaitAnimating(long duration,
             @Nullable IRemoteMagnificationAnimationCallback callback) {
-        resetMockObjects();
-        mWindowMagnificationAnimationController.deleteWindowMagnification(callback);
-        advanceTimeBy(duration);
+        mInstrumentation.runOnMainSync(() -> {
+            mWindowMagnificationAnimationController.deleteWindowMagnification(callback);
+            advanceTimeBy(duration);
+        });
     }
 
     private void verifyStartValue(ArgumentCaptor<Float> captor, float startValue) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
index b8d2bdb..06421db 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
@@ -45,6 +45,7 @@
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.times;
@@ -53,6 +54,7 @@
 
 import android.animation.ValueAnimator;
 import android.annotation.IdRes;
+import android.annotation.Nullable;
 import android.app.Instrumentation;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
@@ -89,6 +91,7 @@
 
 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.AnimatorTestRule;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.res.R;
 import com.android.systemui.settings.FakeDisplayTracker;
@@ -102,6 +105,7 @@
 import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Ignore;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Answers;
@@ -111,8 +115,6 @@
 import org.mockito.MockitoAnnotations;
 
 import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
 @LargeTest
@@ -120,12 +122,10 @@
 @RunWith(AndroidTestingRunner.class)
 public class WindowMagnificationControllerTest extends SysuiTestCase {
 
+    @Rule
+    public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+
     private static final int LAYOUT_CHANGE_TIMEOUT_MS = 5000;
-    // The duration couldn't too short, otherwise the animation check on bounce effect
-    // won't work in expectation. (b/299537784)
-    private static final int BOUNCE_EFFECT_DURATION_MS = 2000;
-    private static final long ANIMATION_DURATION_MS = 300;
-    private final long mWaitingAnimationPeriod = 2 * ANIMATION_DURATION_MS;
     @Mock
     private SfVsyncFrameCallbackProvider mSfVsyncFrameProvider;
     @Mock
@@ -134,11 +134,16 @@
     private WindowMagnifierCallback mWindowMagnifierCallback;
     @Mock
     IRemoteMagnificationAnimationCallback mAnimationCallback;
+    @Mock
+    IRemoteMagnificationAnimationCallback mAnimationCallback2;
     @Mock(answer = Answers.RETURNS_DEEP_STUBS)
     private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
     @Mock
     private SecureSettings mSecureSettings;
 
+    private long mWaitAnimationDuration;
+    private long mWaitBounceEffectDuration;
+
     private Handler mHandler;
     private TestableWindowManager mWindowManager;
     private SysUiState mSysUiState;
@@ -194,6 +199,13 @@
             mResources.getConfiguration().orientation = ORIENTATION_PORTRAIT;
         }
 
+        // Using the animation duration in WindowMagnificationAnimationController for testing.
+        mWaitAnimationDuration = mResources.getInteger(
+                com.android.internal.R.integer.config_longAnimTime);
+        // Using the bounce effect duration in WindowMagnificationController for testing.
+        mWaitBounceEffectDuration = mResources.getInteger(
+                com.android.internal.R.integer.config_shortAnimTime);
+
         mWindowMagnificationAnimationController = new WindowMagnificationAnimationController(
                 mContext, mValueAnimator);
         mWindowMagnificationController =
@@ -208,7 +220,6 @@
                         mSysUiState,
                         () -> mWindowSessionSpy,
                         mSecureSettings);
-        mWindowMagnificationController.setBounceEffectDuration(BOUNCE_EFFECT_DURATION_MS);
 
         verify(mMirrorWindowControl).setWindowDelegate(
                 any(MirrorWindowControl.MirrorWindowDelegate.class));
@@ -281,9 +292,9 @@
                     /* magnificationFrameOffsetRatioY= */ 0,
                     Mockito.mock(IRemoteMagnificationAnimationCallback.class));
         });
+        advanceTimeBy(LAYOUT_CHANGE_TIMEOUT_MS);
 
-        verify(mSfVsyncFrameProvider,
-                timeout(LAYOUT_CHANGE_TIMEOUT_MS).atLeast(2)).postFrameCallback(any());
+        verify(mSfVsyncFrameProvider, atLeast(2)).postFrameCallback(any());
     }
 
     @Test
@@ -401,14 +412,12 @@
 
     @Test
     public void moveWindowMagnifierToPositionWithAnimation_expectedValuesAndInvokeCallback()
-            throws InterruptedException {
+            throws RemoteException {
         mInstrumentation.runOnMainSync(() -> {
             mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN,
                     Float.NaN, 0, 0, null);
         });
-        final CountDownLatch countDownLatch = new CountDownLatch(1);
-        final MockMagnificationAnimationCallback animationCallback =
-                new MockMagnificationAnimationCallback(countDownLatch);
+
         final ArgumentCaptor<Rect> sourceBoundsCaptor = ArgumentCaptor.forClass(Rect.class);
         verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS))
                 .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture());
@@ -417,12 +426,12 @@
 
         mInstrumentation.runOnMainSync(() -> {
             mWindowMagnificationController.moveWindowMagnifierToPosition(
-                    targetCenterX, targetCenterY, animationCallback);
+                    targetCenterX, targetCenterY, mAnimationCallback);
         });
+        advanceTimeBy(mWaitAnimationDuration);
 
-        assertTrue(countDownLatch.await(mWaitingAnimationPeriod, TimeUnit.MILLISECONDS));
-        assertEquals(1, animationCallback.getSuccessCount());
-        assertEquals(0, animationCallback.getFailedCount());
+        verify(mAnimationCallback, times(1)).onResult(eq(true));
+        verify(mAnimationCallback, never()).onResult(eq(false));
         verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS))
                 .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture());
         assertEquals(mWindowMagnificationController.getCenterX(),
@@ -435,14 +444,12 @@
 
     @Test
     public void moveWindowMagnifierToPositionMultipleTimes_expectedValuesAndInvokeCallback()
-            throws InterruptedException {
+            throws RemoteException {
         mInstrumentation.runOnMainSync(() -> {
             mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN,
                     Float.NaN, 0, 0, null);
         });
-        final CountDownLatch countDownLatch = new CountDownLatch(4);
-        final MockMagnificationAnimationCallback animationCallback =
-                new MockMagnificationAnimationCallback(countDownLatch);
+
         final ArgumentCaptor<Rect> sourceBoundsCaptor = ArgumentCaptor.forClass(Rect.class);
         verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS))
                 .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture());
@@ -451,20 +458,20 @@
 
         mInstrumentation.runOnMainSync(() -> {
             mWindowMagnificationController.moveWindowMagnifierToPosition(
-                    centerX + 10, centerY + 10, animationCallback);
+                    centerX + 10, centerY + 10, mAnimationCallback);
             mWindowMagnificationController.moveWindowMagnifierToPosition(
-                    centerX + 20, centerY + 20, animationCallback);
+                    centerX + 20, centerY + 20, mAnimationCallback);
             mWindowMagnificationController.moveWindowMagnifierToPosition(
-                    centerX + 30, centerY + 30, animationCallback);
+                    centerX + 30, centerY + 30, mAnimationCallback);
             mWindowMagnificationController.moveWindowMagnifierToPosition(
-                    centerX + 40, centerY + 40, animationCallback);
+                    centerX + 40, centerY + 40, mAnimationCallback2);
         });
+        advanceTimeBy(mWaitAnimationDuration);
 
-        assertTrue(countDownLatch.await(mWaitingAnimationPeriod, TimeUnit.MILLISECONDS));
         // only the last one callback will return true
-        assertEquals(1, animationCallback.getSuccessCount());
+        verify(mAnimationCallback2).onResult(eq(true));
         // the others will return false
-        assertEquals(3, animationCallback.getFailedCount());
+        verify(mAnimationCallback, times(3)).onResult(eq(false));
         verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS))
                 .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture());
         assertEquals(mWindowMagnificationController.getCenterX(),
@@ -1078,27 +1085,16 @@
 
         final View mirrorView = mWindowManager.getAttachedView();
 
-        final long timeout = SystemClock.uptimeMillis() + 5000;
         final AtomicDouble maxScaleX = new AtomicDouble();
-        final Runnable onAnimationFrame = new Runnable() {
-            @Override
-            public void run() {
-                // For some reason the fancy way doesn't compile...
-//                maxScaleX.getAndAccumulate(mirrorView.getScaleX(), Math::max);
-                final double oldMax = maxScaleX.get();
-                final double newMax = Math.max(mirrorView.getScaleX(), oldMax);
-                assertTrue(maxScaleX.compareAndSet(oldMax, newMax));
+        advanceTimeBy(mWaitBounceEffectDuration, /* runnableOnEachRefresh= */ () -> {
+            // For some reason the fancy way doesn't compile...
+            // maxScaleX.getAndAccumulate(mirrorView.getScaleX(), Math::max);
+            final double oldMax = maxScaleX.get();
+            final double newMax = Math.max(mirrorView.getScaleX(), oldMax);
+            assertTrue(maxScaleX.compareAndSet(oldMax, newMax));
+        });
 
-                if (SystemClock.uptimeMillis() < timeout) {
-                    mirrorView.postOnAnimation(this);
-                }
-            }
-        };
-        mirrorView.postOnAnimation(onAnimationFrame);
-
-        waitForIdleSync();
-
-        ReferenceTestUtils.waitForCondition(() -> maxScaleX.get() > 1.0);
+        assertTrue(maxScaleX.get() > 1.0);
     }
 
     @Test
@@ -1278,7 +1274,8 @@
 
         final float magnificationScaleLarge = 2.5f;
         final int initSize = Math.min(bounds.width(), bounds.height()) / 3;
-        final int magnificationSize = (int) (initSize * magnificationScaleLarge);
+        final int magnificationSize = (int) (initSize * magnificationScaleLarge)
+                - (int) (initSize * magnificationScaleLarge) % 2;
 
         final int expectedWindowHeight = magnificationSize;
         final int expectedWindowWidth = magnificationSize;
@@ -1454,4 +1451,23 @@
         return newRotation;
     }
 
+    // advance time based on the device frame refresh rate
+    private void advanceTimeBy(long timeDelta) {
+        advanceTimeBy(timeDelta, /* runnableOnEachRefresh= */ null);
+    }
+
+    // advance time based on the device frame refresh rate, and trigger runnable on each refresh
+    private void advanceTimeBy(long timeDelta, @Nullable Runnable runnableOnEachRefresh) {
+        final float frameRate = mContext.getDisplay().getRefreshRate();
+        final int timeSlot = (int) (1000 / frameRate);
+        int round = (int) Math.ceil((double) timeDelta / timeSlot);
+        for (; round >= 0; round--) {
+            mInstrumentation.runOnMainSync(() -> {
+                mAnimatorTestRule.advanceTimeBy(timeSlot);
+                if (runnableOnEachRefresh != null) {
+                    runnableOnEachRefresh.run();
+                }
+            });
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
index ec6ec63..e832940 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
@@ -234,10 +234,12 @@
 
         mMenuAnimationController.springMenuWith(DynamicAnimation.TRANSLATION_X, new SpringForce()
                 .setStiffness(stiffness)
-                .setDampingRatio(dampingRatio), velocity, finalPosition);
+                .setDampingRatio(dampingRatio), velocity, finalPosition,
+                /* writeToPosition = */ true);
         mMenuAnimationController.springMenuWith(DynamicAnimation.TRANSLATION_Y, new SpringForce()
                 .setStiffness(stiffness)
-                .setDampingRatio(dampingRatio), velocity, finalPosition);
+                .setDampingRatio(dampingRatio), velocity, finalPosition,
+                /* writeToPosition = */ true);
     }
 
     private void skipAnimationToEnd(DynamicAnimation animation) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
index aed795a..76094c1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
@@ -42,6 +42,10 @@
 import android.graphics.Rect;
 import android.os.Build;
 import android.os.UserHandle;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.provider.Settings;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
@@ -52,8 +56,10 @@
 import android.view.accessibility.AccessibilityManager;
 
 import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.SpringAnimation;
 import androidx.test.filters.SmallTest;
 
+import com.android.systemui.Flags;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.util.settings.SecureSettings;
 
@@ -98,6 +104,10 @@
     @Rule
     public MockitoRule mockito = MockitoJUnit.rule();
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Mock
     private IAccessibilityFloatingMenu mFloatingMenu;
 
@@ -116,7 +126,8 @@
         final Rect mDisplayBounds = new Rect();
         mDisplayBounds.set(/* left= */ 0, /* top= */ 0, DISPLAY_WINDOW_WIDTH,
                 DISPLAY_WINDOW_HEIGHT);
-        mWindowMetrics = spy(new WindowMetrics(mDisplayBounds, fakeDisplayInsets()));
+        mWindowMetrics = spy(
+                new WindowMetrics(mDisplayBounds, fakeDisplayInsets(), /* density = */ 0.0f));
         doReturn(mWindowMetrics).when(mStubWindowManager).getCurrentWindowMetrics();
 
         mMenuViewLayer = new MenuViewLayer(mContext, mStubWindowManager, mStubAccessibilityManager,
@@ -221,7 +232,8 @@
     }
 
     @Test
-    public void showingImeInsetsChange_overlapOnIme_menuShownAboveIme() {
+    @RequiresFlagsDisabled(Flags.FLAG_FLOATING_MENU_IME_DISPLACEMENT_ANIMATION)
+    public void showingImeInsetsChange_overlapOnIme_menuShownAboveIme_old() {
         mMenuAnimationController.moveAndPersistPosition(new PointF(0, IME_TOP + 100));
         final PointF beforePosition = mMenuView.getMenuPosition();
 
@@ -233,15 +245,49 @@
     }
 
     @Test
-    public void hidingImeInsetsChange_overlapOnIme_menuBackToOriginalPosition() {
-        final float menuTop = IME_TOP + 200;
-        mMenuAnimationController.moveAndPersistPosition(new PointF(0, menuTop));
+    @RequiresFlagsEnabled(Flags.FLAG_FLOATING_MENU_IME_DISPLACEMENT_ANIMATION)
+    public void showingImeInsetsChange_overlapOnIme_menuShownAboveIme() {
+        mMenuAnimationController.moveAndPersistPosition(new PointF(0, IME_TOP + 100));
+        final PointF beforePosition = mMenuView.getMenuPosition();
+
         dispatchShowingImeInsets();
+        assertThat(isPositionAnimationRunning()).isTrue();
+        skipPositionAnimations();
+
+        final float menuBottom = mMenuView.getTranslationY() + mMenuView.getMenuHeight();
+
+        assertThat(mMenuView.getTranslationX()).isEqualTo(beforePosition.x);
+        assertThat(menuBottom).isLessThan(beforePosition.y);
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_FLOATING_MENU_IME_DISPLACEMENT_ANIMATION)
+    public void hidingImeInsetsChange_overlapOnIme_menuBackToOriginalPosition_old() {
+        mMenuAnimationController.moveAndPersistPosition(new PointF(0, IME_TOP + 200));
+        final PointF beforePosition = mMenuView.getMenuPosition();
 
         dispatchHidingImeInsets();
 
-        assertThat(mMenuView.getTranslationX()).isEqualTo(0);
-        assertThat(mMenuView.getTranslationY()).isEqualTo(menuTop);
+        assertThat(mMenuView.getTranslationX()).isEqualTo(beforePosition.x);
+        assertThat(mMenuView.getTranslationY()).isEqualTo(beforePosition.y);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_FLOATING_MENU_IME_DISPLACEMENT_ANIMATION)
+    public void hidingImeInsetsChange_overlapOnIme_menuBackToOriginalPosition() {
+        mMenuAnimationController.moveAndPersistPosition(new PointF(0, IME_TOP + 200));
+        final PointF beforePosition = mMenuView.getMenuPosition();
+
+        dispatchShowingImeInsets();
+        assertThat(isPositionAnimationRunning()).isTrue();
+        skipPositionAnimations();
+
+        dispatchHidingImeInsets();
+        assertThat(isPositionAnimationRunning()).isTrue();
+        skipPositionAnimations();
+
+        assertThat(mMenuView.getTranslationX()).isEqualTo(beforePosition.x);
+        assertThat(mMenuView.getTranslationY()).isEqualTo(beforePosition.y);
     }
 
     private void setupEnabledAccessibilityServiceList() {
@@ -294,4 +340,21 @@
                         Insets.of(/* left= */ 0, /* top= */ 0, /* right= */ 0, bottom))
                 .build();
     }
+
+    private boolean isPositionAnimationRunning() {
+        return !mMenuAnimationController.mPositionAnimations.values().stream().filter(
+                        (animation) -> animation.isRunning()).findAny().isEmpty();
+    }
+
+    private void skipPositionAnimations() {
+        mMenuAnimationController.mPositionAnimations.values().stream().forEach(
+                (animation) -> {
+                    final SpringAnimation springAnimation = ((SpringAnimation) animation);
+                    // The doAnimationFrame function is used for skipping animation to the end.
+                    springAnimation.doAnimationFrame(500);
+                    springAnimation.skipToEnd();
+                    springAnimation.doAnimationFrame(500);
+                });
+
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/utils/FlagUtils.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/utils/FlagUtils.java
index 2975549..c7bb0f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/utils/FlagUtils.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/utils/FlagUtils.java
@@ -31,5 +31,6 @@
      */
     public static void setFlagDefaults(SetFlagsRule setFlagsRule) {
         setFlagDefault(setFlagsRule, Flags.FLAG_FLOATING_MENU_OVERLAPS_NAV_BARS_FLAG);
+        setFlagDefault(setFlagsRule, Flags.FLAG_FLOATING_MENU_IME_DISPLACEMENT_ANIMATION);
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/ui/ConfigurationStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/common/ui/ConfigurationStateTest.kt
new file mode 100644
index 0000000..034b802
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/common/ui/ConfigurationStateTest.kt
@@ -0,0 +1,158 @@
+/*
+ * 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
+
+import android.content.Context
+import android.testing.AndroidTestingRunner
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.util.mockito.captureMany
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.verify
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ConfigurationStateTest : SysuiTestCase() {
+
+    private val configurationController: ConfigurationController = mock()
+    private val layoutInflater = TestLayoutInflater()
+
+    val underTest = ConfigurationState(configurationController, context, layoutInflater)
+
+    @Test
+    fun reinflateAndBindLatest_inflatesWithoutEmission() = runTest {
+        var callbackCount = 0
+        backgroundScope.launch {
+            underTest.reinflateAndBindLatest<View>(
+                resource = 0,
+                root = null,
+                attachToRoot = false,
+            ) {
+                callbackCount++
+                null
+            }
+        }
+
+        // Inflates without an emission
+        runCurrent()
+        assertThat(layoutInflater.inflationCount).isEqualTo(1)
+        assertThat(callbackCount).isEqualTo(1)
+    }
+
+    @Test
+    fun reinflateAndBindLatest_reinflatesOnThemeChanged() = runTest {
+        var callbackCount = 0
+        backgroundScope.launch {
+            underTest.reinflateAndBindLatest<View>(
+                resource = 0,
+                root = null,
+                attachToRoot = false,
+            ) {
+                callbackCount++
+                null
+            }
+        }
+        runCurrent()
+
+        val configListeners: List<ConfigurationController.ConfigurationListener> = captureMany {
+            verify(configurationController, atLeastOnce()).addCallback(capture())
+        }
+
+        listOf(1, 2, 3).forEach { count ->
+            assertThat(layoutInflater.inflationCount).isEqualTo(count)
+            assertThat(callbackCount).isEqualTo(count)
+            configListeners.forEach { it.onThemeChanged() }
+            runCurrent()
+        }
+    }
+
+    @Test
+    fun reinflateAndBindLatest_reinflatesOnDensityOrFontScaleChanged() = runTest {
+        var callbackCount = 0
+        backgroundScope.launch {
+            underTest.reinflateAndBindLatest<View>(
+                resource = 0,
+                root = null,
+                attachToRoot = false,
+            ) {
+                callbackCount++
+                null
+            }
+        }
+        runCurrent()
+
+        val configListeners: List<ConfigurationController.ConfigurationListener> = captureMany {
+            verify(configurationController, atLeastOnce()).addCallback(capture())
+        }
+
+        listOf(1, 2, 3).forEach { count ->
+            assertThat(layoutInflater.inflationCount).isEqualTo(count)
+            assertThat(callbackCount).isEqualTo(count)
+            configListeners.forEach { it.onDensityOrFontScaleChanged() }
+            runCurrent()
+        }
+    }
+
+    @Test
+    fun testReinflateAndBindLatest_disposesOnCancel() = runTest {
+        var callbackCount = 0
+        var disposed = false
+        val job = launch {
+            underTest.reinflateAndBindLatest<View>(
+                resource = 0,
+                root = null,
+                attachToRoot = false,
+            ) {
+                callbackCount++
+                DisposableHandle { disposed = true }
+            }
+        }
+
+        runCurrent()
+        job.cancelAndJoin()
+        assertThat(disposed).isTrue()
+    }
+
+    inner class TestLayoutInflater : LayoutInflater(context) {
+
+        var inflationCount = 0
+
+        override fun inflate(resource: Int, root: ViewGroup?, attachToRoot: Boolean): View {
+            inflationCount++
+            return View(context)
+        }
+
+        override fun cloneInContext(p0: Context?): LayoutInflater {
+            // not needed for this test
+            return this
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandlerTest.kt
index 1b2fc93d..2f4fc96 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandlerTest.kt
@@ -67,6 +67,7 @@
                 isAttachedToWindow = { isAttachedToWindow },
                 onLongPressDetected = onLongPressDetected,
                 onSingleTapDetected = onSingleTapDetected,
+                longPressDuration = { ViewConfiguration.getLongPressTimeout().toLong() }
             )
         underTest.isLongPressHandlingEnabled = true
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/SetFlagsRuleExtensions.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/SetFlagsRuleExtensions.kt
index e16b8d4..f4d2cfd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/SetFlagsRuleExtensions.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/SetFlagsRuleExtensions.kt
@@ -19,15 +19,15 @@
 import android.platform.test.flag.junit.SetFlagsRule
 
 /**
- * Set the given flag's value to the real value for the current build configuration.
- * This prevents test code from crashing because it is reading an unspecified flag value.
+ * Set the given flag's value to the real value for the current build configuration. This prevents
+ * test code from crashing because it is reading an unspecified flag value.
  *
- * REMINDER: You should always test your code with your flag in both configurations, so
- * generally you should be explicitly enabling or disabling your flag. This method is for
- * situations where the flag needs to be read (e.g. in the class constructor), but its value
- * shouldn't affect the actual test cases. In those cases, it's mildly safer to use this method
- * than to hard-code `false` or `true` because then at least if you're wrong, and the flag value
- * *does* matter, you'll notice when the flag is flipped and tests start failing.
+ * REMINDER: You should always test your code with your flag in both configurations, so generally
+ * you should be explicitly enabling or disabling your flag. This method is for situations where the
+ * flag needs to be read (e.g. in the class constructor), but its value shouldn't affect the actual
+ * test cases. In those cases, it's mildly safer to use this method than to hard-code `false` or
+ * `true` because then at least if you're wrong, and the flag value *does* matter, you'll notice
+ * when the flag is flipped and tests start failing.
  */
 fun SetFlagsRule.setFlagDefault(flagName: String) {
     if (getFlagDefault(flagName)) {
@@ -37,6 +37,19 @@
     }
 }
 
+/**
+ * Set the given flag to an explicit value, or, if null, to the real value for the current build
+ * configuration. This allows for convenient provisioning in tests where certain tests don't care
+ * what the value is (`setFlagValue(FLAG_FOO, null)`), and others want an explicit value.
+ */
+fun SetFlagsRule.setFlagValue(name: String, value: Boolean?) {
+    when (value) {
+        null -> setFlagDefault(name)
+        true -> enableFlags(name)
+        false -> disableFlags(name)
+    }
+}
+
 // NOTE: This code uses reflection to gain access to private members of aconfig generated
 //  classes (in the same way SetFlagsRule does internally) because this is the only way to get
 //  at the underlying information and read the current value of the flag.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
index ef03fdf..6c4bb37 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
@@ -216,6 +216,29 @@
         }
 
     @Test
+    fun isKeyguardUnlocked() =
+        testScope.runTest {
+            whenever(keyguardStateController.isUnlocked).thenReturn(false)
+            val isKeyguardUnlocked by collectLastValue(underTest.isKeyguardUnlocked)
+
+            runCurrent()
+            assertThat(isKeyguardUnlocked).isFalse()
+
+            val captor = argumentCaptor<KeyguardStateController.Callback>()
+            verify(keyguardStateController, atLeastOnce()).addCallback(captor.capture())
+
+            whenever(keyguardStateController.isUnlocked).thenReturn(true)
+            captor.value.onUnlockedChanged()
+            runCurrent()
+            assertThat(isKeyguardUnlocked).isTrue()
+
+            whenever(keyguardStateController.isUnlocked).thenReturn(false)
+            captor.value.onKeyguardShowingChanged()
+            runCurrent()
+            assertThat(isKeyguardUnlocked).isFalse()
+        }
+
+    @Test
     fun isDozing() =
         testScope.runTest {
             underTest.setIsDozing(true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
index b32905f..27325d3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
@@ -68,7 +68,6 @@
             powerInteractor = PowerInteractorFactory.create().powerInteractor,
             featureFlags = featureFlags,
             sceneContainerFlags = testUtils.sceneContainerFlags,
-            deviceEntryRepository = testUtils.deviceEntryRepository,
             bouncerRepository = bouncerRepository,
             configurationRepository = configurationRepository,
             shadeRepository = shadeRepository,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt
index a5d7457..6cd7406 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt
@@ -35,6 +35,7 @@
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -46,6 +47,7 @@
 import org.mockito.Mockito.verify
 import org.mockito.junit.MockitoJUnit
 
+@ExperimentalCoroutinesApi
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class KeyguardKeyEventInteractorTest : SysuiTestCase() {
@@ -128,58 +130,62 @@
     }
 
     @Test
-    fun dispatchKeyEvent_menuActionUp_interactiveKeyguard_collapsesShade() {
+    fun dispatchKeyEvent_menuActionUp_awakeKeyguard_showsPrimaryBouncer() {
         powerInteractor.setAwakeForTest()
         whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
         whenever(statusBarKeyguardViewManager.shouldDismissOnMenuPressed()).thenReturn(true)
 
-        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU)
-        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue()
-        verify(shadeController).animateCollapseShadeForced()
+        verifyActionUpShowsPrimaryBouncer(KeyEvent.KEYCODE_MENU)
     }
 
     @Test
-    fun dispatchKeyEvent_menuActionUp_interactiveShadeLocked_collapsesShade() {
+    fun dispatchKeyEvent_menuActionUp_awakeShadeLocked_collapsesShade() {
         powerInteractor.setAwakeForTest()
         whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED)
         whenever(statusBarKeyguardViewManager.shouldDismissOnMenuPressed()).thenReturn(true)
 
-        // action down: does NOT collapse the shade
-        val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MENU)
-        assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse()
-        verify(shadeController, never()).animateCollapseShadeForced()
-
-        // action up: collapses the shade
-        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU)
-        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue()
-        verify(shadeController).animateCollapseShadeForced()
+        verifyActionUpCollapsesTheShade(KeyEvent.KEYCODE_MENU)
     }
 
     @Test
-    fun dispatchKeyEvent_menuActionUp_nonInteractiveKeyguard_neverCollapsesShade() {
+    fun dispatchKeyEvent_menuActionUp_asleepKeyguard_neverCollapsesShade() {
         powerInteractor.setAsleepForTest()
         whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
         whenever(statusBarKeyguardViewManager.shouldDismissOnMenuPressed()).thenReturn(true)
 
-        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU)
-        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isFalse()
-        verify(shadeController, never()).animateCollapseShadeForced()
+        verifyActionsDoNothing(KeyEvent.KEYCODE_MENU)
     }
 
     @Test
-    fun dispatchKeyEvent_spaceActionUp_interactiveKeyguard_collapsesShade() {
+    fun dispatchKeyEvent_spaceActionUp_awakeKeyguard_collapsesShade() {
         powerInteractor.setAwakeForTest()
         whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
 
-        // action down: does NOT collapse the shade
-        val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SPACE)
-        assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse()
-        verify(shadeController, never()).animateCollapseShadeForced()
+        verifyActionUpShowsPrimaryBouncer(KeyEvent.KEYCODE_SPACE)
+    }
 
-        // action up: collapses the shade
-        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_SPACE)
-        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue()
-        verify(shadeController).animateCollapseShadeForced()
+    @Test
+    fun dispatchKeyEvent_spaceActionUp_shadeLocked_collapsesShade() {
+        powerInteractor.setAwakeForTest()
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED)
+
+        verifyActionUpCollapsesTheShade(KeyEvent.KEYCODE_SPACE)
+    }
+
+    @Test
+    fun dispatchKeyEvent_enterActionUp_awakeKeyguard_showsPrimaryBouncer() {
+        powerInteractor.setAwakeForTest()
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+
+        verifyActionUpShowsPrimaryBouncer(KeyEvent.KEYCODE_ENTER)
+    }
+
+    @Test
+    fun dispatchKeyEvent_enterActionUp_shadeLocked_collapsesShade() {
+        powerInteractor.setAwakeForTest()
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED)
+
+        verifyActionUpCollapsesTheShade(KeyEvent.KEYCODE_ENTER)
     }
 
     @Test
@@ -249,4 +255,42 @@
             .isFalse()
         verify(statusBarKeyguardViewManager, never()).interceptMediaKey(any())
     }
+
+    private fun verifyActionUpCollapsesTheShade(keycode: Int) {
+        // action down: does NOT collapse the shade
+        val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, keycode)
+        assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse()
+        verify(shadeController, never()).animateCollapseShadeForced()
+
+        // action up: collapses the shade
+        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, keycode)
+        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue()
+        verify(shadeController).animateCollapseShadeForced()
+    }
+
+    private fun verifyActionUpShowsPrimaryBouncer(keycode: Int) {
+        // action down: does NOT collapse the shade
+        val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, keycode)
+        assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse()
+        verify(statusBarKeyguardViewManager, never()).showPrimaryBouncer(any())
+
+        // action up: collapses the shade
+        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, keycode)
+        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue()
+        verify(statusBarKeyguardViewManager).showPrimaryBouncer(eq(true))
+    }
+
+    private fun verifyActionsDoNothing(keycode: Int) {
+        // action down: does nothing
+        val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, keycode)
+        assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse()
+        verify(shadeController, never()).animateCollapseShadeForced()
+        verify(statusBarKeyguardViewManager, never()).showPrimaryBouncer(any())
+
+        // action up: doesNothing
+        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, keycode)
+        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isFalse()
+        verify(shadeController, never()).animateCollapseShadeForced()
+        verify(statusBarKeyguardViewManager, never()).showPrimaryBouncer(any())
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt
index 50ee026..8cfa87d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt
@@ -30,8 +30,8 @@
 import com.android.systemui.keyguard.ui.view.layout.sections.AodBurnInSection
 import com.android.systemui.keyguard.ui.view.layout.sections.AodNotificationIconsSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultAmbientIndicationAreaSection
+import com.android.systemui.keyguard.ui.view.layout.sections.DefaultDeviceEntryIconSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultIndicationAreaSection
-import com.android.systemui.keyguard.ui.view.layout.sections.DefaultLockIconSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultNotificationStackScrollLayoutSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultSettingsPopupMenuSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultShortcutsSection
@@ -55,7 +55,7 @@
     private lateinit var underTest: DefaultKeyguardBlueprint
     private lateinit var rootView: KeyguardRootView
     @Mock private lateinit var defaultIndicationAreaSection: DefaultIndicationAreaSection
-    @Mock private lateinit var defaultLockIconSection: DefaultLockIconSection
+    @Mock private lateinit var mDefaultDeviceEntryIconSection: DefaultDeviceEntryIconSection
     @Mock private lateinit var defaultShortcutsSection: DefaultShortcutsSection
     @Mock
     private lateinit var defaultAmbientIndicationAreaSection: DefaultAmbientIndicationAreaSection
@@ -75,7 +75,7 @@
         underTest =
             DefaultKeyguardBlueprint(
                 defaultIndicationAreaSection,
-                defaultLockIconSection,
+                mDefaultDeviceEntryIconSection,
                 defaultShortcutsSection,
                 defaultAmbientIndicationAreaSection,
                 defaultSettingsPopupMenuSection,
@@ -101,14 +101,14 @@
         val prevBlueprint = mock(KeyguardBlueprint::class.java)
         val someSection = mock(KeyguardSection::class.java)
         whenever(prevBlueprint.sections)
-            .thenReturn(underTest.sections.minus(defaultLockIconSection).plus(someSection))
+            .thenReturn(underTest.sections.minus(mDefaultDeviceEntryIconSection).plus(someSection))
         val constraintLayout = ConstraintLayout(context, null)
         underTest.replaceViews(prevBlueprint, constraintLayout)
-        underTest.sections.minus(defaultLockIconSection).forEach {
+        underTest.sections.minus(mDefaultDeviceEntryIconSection).forEach {
             verify(it, never()).addViews(constraintLayout)
         }
 
-        verify(defaultLockIconSection).addViews(constraintLayout)
+        verify(mDefaultDeviceEntryIconSection).addViews(constraintLayout)
         verify(someSection).removeViews(constraintLayout)
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultLockIconSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSectionTest.kt
similarity index 65%
rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultLockIconSectionTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSectionTest.kt
index 7495637..5f22c7d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultLockIconSectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSectionTest.kt
@@ -24,14 +24,17 @@
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.LockIconViewController
-import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.AuthController
-import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.FakeFeatureFlagsClassic
 import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.res.R
 import com.android.systemui.shade.NotificationPanelView
-import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -40,35 +43,54 @@
 import org.mockito.Mock
 import org.mockito.MockitoAnnotations
 
+@ExperimentalCoroutinesApi
 @RunWith(JUnit4::class)
 @SmallTest
-class DefaultLockIconSectionTest : SysuiTestCase() {
+class DefaultDeviceEntryIconSectionTest : SysuiTestCase() {
     @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
     @Mock private lateinit var authController: AuthController
     @Mock(answer = Answers.RETURNS_DEEP_STUBS) private lateinit var windowManager: WindowManager
     @Mock private lateinit var notificationPanelView: NotificationPanelView
-    @Mock private lateinit var featureFlags: FeatureFlags
+    private lateinit var featureFlags: FakeFeatureFlags
     @Mock private lateinit var lockIconViewController: LockIconViewController
-    private lateinit var underTest: DefaultLockIconSection
+    @Mock private lateinit var falsingManager: FalsingManager
+    private lateinit var underTest: DefaultDeviceEntryIconSection
 
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
+        featureFlags =
+            FakeFeatureFlagsClassic().apply {
+                set(Flags.MIGRATE_LOCK_ICON, false)
+                set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, false)
+                set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, false)
+            }
         underTest =
-            DefaultLockIconSection(
+            DefaultDeviceEntryIconSection(
                 keyguardUpdateMonitor,
                 authController,
                 windowManager,
                 context,
                 notificationPanelView,
                 featureFlags,
-                lockIconViewController
+                { lockIconViewController },
+                { DeviceEntryIconViewModel() },
+                { falsingManager },
             )
     }
 
     @Test
-    fun addViewsConditionally() {
-        whenever(featureFlags.isEnabled(Flags.MIGRATE_LOCK_ICON)).thenReturn(true)
+    fun addViewsConditionally_migrateFlagOn() {
+        featureFlags.set(Flags.MIGRATE_LOCK_ICON, true)
+        val constraintLayout = ConstraintLayout(context, null)
+        underTest.addViews(constraintLayout)
+        assertThat(constraintLayout.childCount).isGreaterThan(0)
+    }
+
+    @Test
+    fun addViewsConditionally_migrateAndRefactorFlagsOn() {
+        featureFlags.set(Flags.MIGRATE_LOCK_ICON, true)
+        featureFlags.set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, true)
         val constraintLayout = ConstraintLayout(context, null)
         underTest.addViews(constraintLayout)
         assertThat(constraintLayout.childCount).isGreaterThan(0)
@@ -76,7 +98,8 @@
 
     @Test
     fun addViewsConditionally_migrateFlagOff() {
-        whenever(featureFlags.isEnabled(Flags.MIGRATE_LOCK_ICON)).thenReturn(false)
+        featureFlags.set(Flags.MIGRATE_LOCK_ICON, false)
+        featureFlags.set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, false)
         val constraintLayout = ConstraintLayout(context, null)
         underTest.addViews(constraintLayout)
         assertThat(constraintLayout.childCount).isEqualTo(0)
@@ -87,18 +110,18 @@
         val cs = ConstraintSet()
         underTest.applyConstraints(cs)
 
-        val constraint = cs.getConstraint(R.id.lock_icon_view)
+        val constraint = cs.getConstraint(R.id.device_entry_icon_view)
 
         assertThat(constraint.layout.topToTop).isEqualTo(ConstraintSet.PARENT_ID)
         assertThat(constraint.layout.startToStart).isEqualTo(ConstraintSet.PARENT_ID)
     }
 
     @Test
-    fun testCenterLockIcon() {
+    fun testCenterIcon() {
         val cs = ConstraintSet()
-        underTest.centerLockIcon(Point(5, 6), 1F, cs)
+        underTest.centerIcon(Point(5, 6), 1F, cs)
 
-        val constraint = cs.getConstraint(R.id.lock_icon_view)
+        val constraint = cs.getConstraint(R.id.device_entry_icon_view)
 
         assertThat(constraint.layout.mWidth).isEqualTo(2)
         assertThat(constraint.layout.mHeight).isEqualTo(2)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt
index deefab6..b101acf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt
@@ -124,10 +124,10 @@
                 context,
                 controllerFactory,
                 lmmFactory,
-                mr2,
+                { mr2 },
                 muteAwaitFactory,
                 configurationController,
-                localBluetoothManager,
+                { localBluetoothManager },
                 fakeFgExecutor,
                 fakeBgExecutor,
                 dumpster,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/MediaProjectionMetricsLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/MediaProjectionMetricsLoggerTest.kt
index fd1e2c7..da448aa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/MediaProjectionMetricsLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/MediaProjectionMetricsLoggerTest.kt
@@ -74,6 +74,15 @@
     }
 
     @Test
+    fun notifyProjectionCancelled_forwardsToServiceWithMetricsValue() {
+        val hostUid = 123
+
+        logger.notifyProjectionRequestCancelled(hostUid)
+
+        verify(service).notifyPermissionRequestCancelled(hostUid)
+    }
+
+    @Test
     fun notifyAppSelectorDisplayed_forwardsToService() {
         val hostUid = 654
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt
index 5255f71..44798ea 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt
@@ -4,7 +4,6 @@
 import android.os.UserHandle
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
-import com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_APP_SELECTOR_DISPLAYED as STATE_APP_SELECTOR_DISPLAYED
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
 import com.android.systemui.mediaprojection.appselector.data.RecentTask
@@ -214,7 +213,7 @@
     @Test
     fun init_firstStart_logsAppSelectorDisplayed() {
         val hostUid = 123456789
-        val controller = createController(isFirstStart = true,  hostUid)
+        val controller = createController(isFirstStart = true, hostUid)
 
         controller.init()
 
@@ -231,6 +230,15 @@
         verify(logger, never()).notifyAppSelectorDisplayed(hostUid)
     }
 
+    @Test
+    fun onSelectorDismissed_logsProjectionRequestCancelled() {
+        val hostUid = 123
+
+        createController(hostUid = hostUid).onSelectorDismissed()
+
+        verify(logger).notifyProjectionRequestCancelled(hostUid)
+    }
+
     private fun givenCaptureAllowed(isAllow: Boolean) {
         whenever(policyResolver.isScreenCaptureAllowed(any(), any())).thenReturn(isAllow)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt
index 0e6e4fa..c711806 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt
@@ -373,18 +373,6 @@
     }
 
     @Test
-    fun isVisible() {
-        val underTest = utils.footerActionsViewModel()
-        assertThat(underTest.isVisible.value).isFalse()
-
-        underTest.onVisibilityChangeRequested(visible = true)
-        assertThat(underTest.isVisible.value).isTrue()
-
-        underTest.onVisibilityChangeRequested(visible = false)
-        assertThat(underTest.isVisible.value).isFalse()
-    }
-
-    @Test
     fun alpha_inSplitShade_followsExpansion() {
         val underTest = utils.footerActionsViewModel()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/interactor/DisabledByPolicyInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/interactor/DisabledByPolicyInteractorTest.kt
index a6199c2..2bdc154 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/interactor/DisabledByPolicyInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/interactor/DisabledByPolicyInteractorTest.kt
@@ -83,7 +83,7 @@
     @Test
     fun testDisabledWhenAdminWithNoRestrictions() =
         testScope.runTest {
-            val admin = EnforcedAdmin(TEST_COMPONENT_NAME, UserHandle(TEST_USER))
+            val admin = EnforcedAdmin(TEST_COMPONENT_NAME, TEST_USER)
             whenever(restrictedLockProxy.getEnforcedAdmin(anyInt(), anyString())).thenReturn(admin)
             whenever(restrictedLockProxy.hasBaseUserRestriction(anyInt(), anyString()))
                 .thenReturn(false)
@@ -129,11 +129,11 @@
     }
 
     private companion object {
-        const val TEST_USER = 1
+
         const val TEST_RESTRICTION = "test_restriction"
 
         val TEST_COMPONENT_NAME = ComponentName("test.pkg", "test.cls")
-
-        val ADMIN = EnforcedAdmin(TEST_COMPONENT_NAME, UserHandle(TEST_USER))
+        val TEST_USER = UserHandle(1)
+        val ADMIN = EnforcedAdmin(TEST_COMPONENT_NAME, TEST_USER)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractorTest.kt
index fc2b7a64..479e62d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractorTest.kt
@@ -47,11 +47,12 @@
 
     @Mock private lateinit var bluetoothAdapter: LocalBluetoothAdapter
     @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
+    @Mock private lateinit var logger: BluetoothTileDialogLogger
 
     @Before
     fun setUp() {
         bluetoothStateInteractor =
-            BluetoothStateInteractor(localBluetoothManager, testScope.backgroundScope)
+            BluetoothStateInteractor(localBluetoothManager, logger, testScope.backgroundScope)
         `when`(localBluetoothManager.bluetoothAdapter).thenReturn(bluetoothAdapter)
     }
 
@@ -80,6 +81,8 @@
 
             bluetoothStateInteractor.isBluetoothEnabled = true
             verify(bluetoothAdapter).enable()
+            verify(logger)
+                .logBluetoothState(BluetoothStateStage.BLUETOOTH_STATE_VALUE_SET, true.toString())
         }
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt
index 8b66040..3b6bfee 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt
@@ -30,6 +30,7 @@
 import com.android.settingslib.bluetooth.CachedBluetoothDevice
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.res.R
+import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Rule
@@ -60,8 +61,12 @@
 
     @Mock private lateinit var uiEventLogger: UiEventLogger
 
+    @Mock private lateinit var logger: BluetoothTileDialogLogger
+
     private val subtitleResId = R.string.quick_settings_bluetooth_tile_subtitle
 
+    private val fakeSystemClock = FakeSystemClock()
+
     private lateinit var icon: Pair<Drawable, String>
     private lateinit var bluetoothTileDialog: BluetoothTileDialog
     private lateinit var deviceItem: DeviceItem
@@ -73,7 +78,9 @@
                 ENABLED,
                 subtitleResId,
                 bluetoothTileDialogCallback,
+                fakeSystemClock,
                 uiEventLogger,
+                logger,
                 mContext
             )
         icon = Pair(drawable, DEVICE_NAME)
@@ -109,7 +116,9 @@
                 ENABLED,
                 subtitleResId,
                 bluetoothTileDialogCallback,
+                fakeSystemClock,
                 uiEventLogger,
+                logger,
                 mContext
             )
         bluetoothTileDialog.show()
@@ -138,7 +147,9 @@
                     ENABLED,
                     subtitleResId,
                     bluetoothTileDialogCallback,
+                    fakeSystemClock,
                     uiEventLogger,
+                    logger,
                     mContext
                 )
                 .Adapter(bluetoothTileDialogCallback)
@@ -162,7 +173,9 @@
                     ENABLED,
                     subtitleResId,
                     bluetoothTileDialogCallback,
+                    fakeSystemClock,
                     uiEventLogger,
+                    logger,
                     mContext
                 )
                 .Adapter(bluetoothTileDialogCallback)
@@ -182,7 +195,9 @@
                 ENABLED,
                 subtitleResId,
                 bluetoothTileDialogCallback,
+                fakeSystemClock,
                 uiEventLogger,
+                logger,
                 mContext
             )
         bluetoothTileDialog.show()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
index dcda005..fb5dd21 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
@@ -77,6 +77,8 @@
 
     @Mock private lateinit var uiEventLogger: UiEventLogger
 
+    @Mock private lateinit var logger: BluetoothTileDialogLogger
+
     private lateinit var scheduler: TestCoroutineScheduler
     private lateinit var dispatcher: CoroutineDispatcher
     private lateinit var testScope: TestScope
@@ -92,7 +94,9 @@
                 bluetoothStateInteractor,
                 dialogLaunchAnimator,
                 activityStarter,
+                fakeSystemClock,
                 uiEventLogger,
+                logger,
                 testScope.backgroundScope,
                 dispatcher,
             )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractorTest.kt
index 428f79c..4c173cc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractorTest.kt
@@ -28,6 +28,7 @@
 import com.android.settingslib.bluetooth.LocalBluetoothManager
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.test.TestScope
@@ -38,6 +39,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
+import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.junit.MockitoJUnit
 import org.mockito.junit.MockitoRule
@@ -71,6 +73,10 @@
 
     @Mock private lateinit var uiEventLogger: UiEventLogger
 
+    @Mock private lateinit var logger: BluetoothTileDialogLogger
+
+    private val fakeSystemClock = FakeSystemClock()
+
     private lateinit var interactor: DeviceItemInteractor
 
     private lateinit var dispatcher: CoroutineDispatcher
@@ -87,13 +93,16 @@
                 audioManager,
                 adapter,
                 localBluetoothManager,
+                fakeSystemClock,
                 uiEventLogger,
+                logger,
                 testScope.backgroundScope,
                 dispatcher
             )
 
         `when`(deviceItem1.cachedBluetoothDevice).thenReturn(cachedDevice1)
         `when`(deviceItem2.cachedBluetoothDevice).thenReturn(cachedDevice2)
+        `when`(cachedDevice1.address).thenReturn("ADDRESS")
         `when`(cachedDevice1.device).thenReturn(device1)
         `when`(cachedDevice2.device).thenReturn(device2)
         `when`(bluetoothTileDialogRepository.cachedDevices)
@@ -109,7 +118,7 @@
             )
 
             val latest by collectLastValue(interactor.deviceItemUpdate)
-            interactor.updateDeviceItems(mContext)
+            interactor.updateDeviceItems(mContext, DeviceFetchTrigger.FIRST_LOAD)
 
             assertThat(latest).isEqualTo(emptyList<DeviceItem>())
         }
@@ -124,7 +133,7 @@
             )
 
             val latest by collectLastValue(interactor.deviceItemUpdate)
-            interactor.updateDeviceItems(mContext)
+            interactor.updateDeviceItems(mContext, DeviceFetchTrigger.FIRST_LOAD)
 
             assertThat(latest).isEqualTo(emptyList<DeviceItem>())
         }
@@ -139,7 +148,7 @@
             )
 
             val latest by collectLastValue(interactor.deviceItemUpdate)
-            interactor.updateDeviceItems(mContext)
+            interactor.updateDeviceItems(mContext, DeviceFetchTrigger.FIRST_LOAD)
 
             assertThat(latest).isEqualTo(listOf(deviceItem1))
         }
@@ -154,7 +163,7 @@
             )
 
             val latest by collectLastValue(interactor.deviceItemUpdate)
-            interactor.updateDeviceItems(mContext)
+            interactor.updateDeviceItems(mContext, DeviceFetchTrigger.FIRST_LOAD)
 
             assertThat(latest).isEqualTo(listOf(deviceItem2, deviceItem2))
         }
@@ -180,7 +189,7 @@
             `when`(deviceItem2.type).thenReturn(DeviceItemType.SAVED_BLUETOOTH_DEVICE)
 
             val latest by collectLastValue(interactor.deviceItemUpdate)
-            interactor.updateDeviceItems(mContext)
+            interactor.updateDeviceItems(mContext, DeviceFetchTrigger.FIRST_LOAD)
 
             assertThat(latest).isEqualTo(listOf(deviceItem2, deviceItem1))
         }
@@ -203,12 +212,55 @@
             `when`(deviceItem2.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
 
             val latest by collectLastValue(interactor.deviceItemUpdate)
-            interactor.updateDeviceItems(mContext)
+            interactor.updateDeviceItems(mContext, DeviceFetchTrigger.FIRST_LOAD)
 
             assertThat(latest).isEqualTo(listOf(deviceItem2, deviceItem1))
         }
     }
 
+    @Test
+    fun testUpdateDeviceItemOnClick_connectedMedia_setActive() {
+        `when`(deviceItem1.type).thenReturn(DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE)
+
+        interactor.updateDeviceItemOnClick(deviceItem1)
+
+        verify(cachedDevice1).setActive()
+        verify(logger)
+            .logDeviceClick(cachedDevice1.address, DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE)
+    }
+
+    @Test
+    fun testUpdateDeviceItemOnClick_activeMedia_disconnect() {
+        `when`(deviceItem1.type).thenReturn(DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE)
+
+        interactor.updateDeviceItemOnClick(deviceItem1)
+
+        verify(cachedDevice1).disconnect()
+        verify(logger)
+            .logDeviceClick(cachedDevice1.address, DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE)
+    }
+
+    @Test
+    fun testUpdateDeviceItemOnClick_connectedOtherDevice_disconnect() {
+        `when`(deviceItem1.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
+
+        interactor.updateDeviceItemOnClick(deviceItem1)
+
+        verify(cachedDevice1).disconnect()
+        verify(logger)
+            .logDeviceClick(cachedDevice1.address, DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
+    }
+
+    @Test
+    fun testUpdateDeviceItemOnClick_saved_connect() {
+        `when`(deviceItem1.type).thenReturn(DeviceItemType.SAVED_BLUETOOTH_DEVICE)
+
+        interactor.updateDeviceItemOnClick(deviceItem1)
+
+        verify(cachedDevice1).connect()
+        verify(logger).logDeviceClick(cachedDevice1.address, DeviceItemType.SAVED_BLUETOOTH_DEVICE)
+    }
+
     private fun createFactory(
         isFilterMatchFunc: (CachedBluetoothDevice) -> Boolean,
         deviceItem: DeviceItem
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigProviderTest.kt
new file mode 100644
index 0000000..682b2d0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigProviderTest.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.qs.tiles.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+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
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class QSTileConfigProviderTest : SysuiTestCase() {
+
+    private val underTest =
+        QSTileConfigProviderImpl(
+            mapOf(VALID_SPEC.spec to QSTileConfigTestBuilder.build { tileSpec = VALID_SPEC })
+        )
+
+    @Test
+    fun providerReturnsConfig() {
+        assertThat(underTest.getConfig(VALID_SPEC.spec)).isNotNull()
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun throwsForInvalidSpec() {
+        underTest.getConfig(INVALID_SPEC.spec)
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun validatesSpecUponCreation() {
+        QSTileConfigProviderImpl(
+            mapOf(VALID_SPEC.spec to QSTileConfigTestBuilder.build { tileSpec = INVALID_SPEC })
+        )
+    }
+
+    private companion object {
+
+        val VALID_SPEC = TileSpec.create("valid_tile_spec")
+        val INVALID_SPEC = TileSpec.create("invalid_tile_spec")
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt
index 9b85012..9bf4a75 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt
@@ -16,22 +16,21 @@
 
 package com.android.systemui.qs.tiles.viewmodel
 
+import android.os.UserHandle
 import android.testing.TestableLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
-import com.android.internal.logging.InstanceId
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingManagerFake
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
 import com.android.systemui.qs.tiles.base.interactor.FakeDisabledByPolicyInteractor
 import com.android.systemui.qs.tiles.base.interactor.FakeQSTileDataInteractor
 import com.android.systemui.qs.tiles.base.interactor.FakeQSTileUserActionInteractor
 import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
 import com.android.systemui.qs.tiles.base.logging.QSTileLogger
-import com.android.systemui.qs.tiles.base.viewmodel.BaseQSTileViewModel
+import com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelImpl
 import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
@@ -77,9 +76,6 @@
         testScope.runTest {
             assertThat(fakeQSTileDataInteractor.dataRequests).isEmpty()
 
-            underTest.onLifecycle(QSTileLifecycle.ALIVE)
-            underTest.onUserIdChanged(1)
-
             assertThat(fakeQSTileDataInteractor.dataRequests).isEmpty()
 
             underTest.state.launchIn(backgroundScope)
@@ -87,20 +83,22 @@
 
             assertThat(fakeQSTileDataInteractor.dataRequests).isNotEmpty()
             assertThat(fakeQSTileDataInteractor.dataRequests.first())
-                .isEqualTo(FakeQSTileDataInteractor.DataRequest(1))
+                .isEqualTo(FakeQSTileDataInteractor.DataRequest(UserHandle.of(0)))
         }
 
     private fun createViewModel(
         scope: TestScope,
         config: QSTileConfig = TEST_QS_TILE_CONFIG,
     ): QSTileViewModel =
-        BaseQSTileViewModel(
+        QSTileViewModelImpl(
             config,
-            fakeQSTileUserActionInteractor,
-            fakeQSTileDataInteractor,
-            object : QSTileDataToStateMapper<Any> {
-                override fun map(config: QSTileConfig, data: Any): QSTileState =
-                    QSTileState.build(Icon.Resource(0, ContentDescription.Resource(0)), "") {}
+            { fakeQSTileUserActionInteractor },
+            { fakeQSTileDataInteractor },
+            {
+                object : QSTileDataToStateMapper<Any> {
+                    override fun map(config: QSTileConfig, data: Any): QSTileState =
+                        QSTileState.build(Icon.Resource(0, ContentDescription.Resource(0)), "") {}
+                }
             },
             fakeDisabledByPolicyInteractor,
             fakeUserRepository,
@@ -114,12 +112,6 @@
 
     private companion object {
 
-        val TEST_QS_TILE_CONFIG =
-            QSTileConfig(
-                TileSpec.create("default"),
-                Icon.Resource(0, null),
-                0,
-                InstanceId.fakeInstanceId(0),
-            )
+        val TEST_QS_TILE_CONFIG = QSTileConfigTestBuilder.build {}
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
index c439cfe..49049bc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.screenrecord;
 
+import static android.os.Process.myUid;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static junit.framework.Assert.assertFalse;
@@ -31,8 +33,8 @@
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
-import android.os.Looper;
 import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
 
 import androidx.test.filters.SmallTest;
 
@@ -60,6 +62,7 @@
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
 /**
  * Tests for exception handling and  bitmap configuration in adding smart actions to Screenshot
  * Notification.
@@ -117,10 +120,6 @@
     // starting, and notifies listeners.
     @Test
     public void testCancelCountdown() {
-        if (Looper.myLooper() == null) {
-            Looper.prepare();
-        }
-
         mController.startCountdown(100, 10, null, null);
 
         assertTrue(mController.isStarting());
@@ -137,10 +136,6 @@
     // Test that when recording is started, the start intent is sent and listeners are notified.
     @Test
     public void testStartRecording() throws PendingIntent.CanceledException {
-        if (Looper.myLooper() == null) {
-            Looper.prepare();
-        }
-
         PendingIntent startIntent = Mockito.mock(PendingIntent.class);
         mController.startCountdown(0, 0, startIntent, null);
 
@@ -151,10 +146,6 @@
     // Test that when recording is stopped, the stop intent is sent and listeners are notified.
     @Test
     public void testStopRecording() throws PendingIntent.CanceledException {
-        if (Looper.myLooper() == null) {
-            Looper.prepare();
-        }
-
         PendingIntent startIntent = Mockito.mock(PendingIntent.class);
         PendingIntent stopIntent = Mockito.mock(PendingIntent.class);
 
@@ -182,10 +173,6 @@
     // Test that broadcast will update state
     @Test
     public void testUpdateStateBroadcast() {
-        if (Looper.myLooper() == null) {
-            Looper.prepare();
-        }
-
         // When a recording has started
         PendingIntent startIntent = Mockito.mock(PendingIntent.class);
         mController.startCountdown(0, 0, startIntent, null);
@@ -211,10 +198,6 @@
     // Test that switching users will stop an ongoing recording
     @Test
     public void testUserChange() {
-        if (Looper.myLooper() == null) {
-            Looper.prepare();
-        }
-
         // If we are recording
         PendingIntent startIntent = Mockito.mock(PendingIntent.class);
         PendingIntent stopIntent = Mockito.mock(PendingIntent.class);
@@ -231,10 +214,6 @@
 
     @Test
     public void testPoliciesFlagDisabled_screenCapturingNotAllowed_returnsNullDevicePolicyDialog() {
-        if (Looper.myLooper() == null) {
-            Looper.prepare();
-        }
-
         mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING, true);
         mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES, false);
         when(mDevicePolicyResolver.isScreenCaptureCompletelyDisabled((any()))).thenReturn(true);
@@ -247,10 +226,6 @@
 
     @Test
     public void testPartialScreenSharingDisabled_returnsLegacyDialog() {
-        if (Looper.myLooper() == null) {
-            Looper.prepare();
-        }
-
         mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING, false);
         mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES, false);
 
@@ -262,10 +237,6 @@
 
     @Test
     public void testPoliciesFlagEnabled_screenCapturingNotAllowed_returnsDevicePolicyDialog() {
-        if (Looper.myLooper() == null) {
-            Looper.prepare();
-        }
-
         mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING, true);
         mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES, true);
         when(mDevicePolicyResolver.isScreenCaptureCompletelyDisabled((any()))).thenReturn(true);
@@ -278,10 +249,6 @@
 
     @Test
     public void testPoliciesFlagEnabled_screenCapturingAllowed_returnsNullDevicePolicyDialog() {
-        if (Looper.myLooper() == null) {
-            Looper.prepare();
-        }
-
         mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING, true);
         mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES, true);
         when(mDevicePolicyResolver.isScreenCaptureCompletelyDisabled((any()))).thenReturn(false);
@@ -294,9 +261,6 @@
 
     @Test
     public void testPoliciesFlagEnabled_screenCapturingAllowed_logsProjectionInitiated() {
-        if (Looper.myLooper() == null) {
-            Looper.prepare();
-        }
         mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING, true);
         mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES, true);
         when(mDevicePolicyResolver.isScreenCaptureCompletelyDisabled((any()))).thenReturn(false);
@@ -306,7 +270,7 @@
 
         verify(mMediaProjectionMetricsLogger)
                 .notifyProjectionInitiated(
-                        TEST_USER_ID,
+                        /* hostUid= */ myUid(),
                         SessionCreationSource.SYSTEM_UI_SCREEN_RECORDER);
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt
index bf12d7d..fd38139 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
 import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorActivity
 import com.android.systemui.mediaprojection.permission.ENTIRE_SCREEN
 import com.android.systemui.mediaprojection.permission.SINGLE_APP
@@ -41,7 +42,6 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
-import org.mockito.Mockito
 import org.mockito.Mockito.eq
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
@@ -57,6 +57,7 @@
     @Mock private lateinit var userContextProvider: UserContextProvider
     @Mock private lateinit var flags: FeatureFlags
     @Mock private lateinit var onStartRecordingClicked: Runnable
+    @Mock private lateinit var mediaProjectionMetricsLogger: MediaProjectionMetricsLogger
 
     private lateinit var dialog: ScreenRecordPermissionDialog
 
@@ -72,7 +73,8 @@
                 controller,
                 starter,
                 userContextProvider,
-                onStartRecordingClicked
+                onStartRecordingClicked,
+                mediaProjectionMetricsLogger,
             )
         dialog.onCreate(null)
         whenever(flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)).thenReturn(true)
@@ -149,6 +151,28 @@
         assertThat(dialog.isShowing).isFalse()
     }
 
+    @Test
+    fun showDialog_cancelClickedMultipleTimes_projectionRequestCancelledIsLoggedOnce() {
+        dialog.show()
+
+        clickOnCancel()
+        clickOnCancel()
+
+        verify(mediaProjectionMetricsLogger).notifyProjectionRequestCancelled(TEST_HOST_UID)
+    }
+
+    @Test
+    fun dismissDialog_dismissCalledMultipleTimes_projectionRequestCancelledIsLoggedOnce() {
+        dialog.show()
+
+        TestableLooper.get(this).runWithLooper {
+            dialog.dismiss()
+            dialog.dismiss()
+        }
+
+        verify(mediaProjectionMetricsLogger).notifyProjectionRequestCancelled(TEST_HOST_UID)
+    }
+
     private fun clickOnCancel() {
         dialog.requireViewById<View>(android.R.id.button2).performClick()
     }
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 6223e25..2ce4b04 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -84,6 +84,7 @@
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.classifier.FalsingManagerFake;
+import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository;
 import com.android.systemui.common.ui.view.LongPressHandlingView;
 import com.android.systemui.doze.DozeLog;
 import com.android.systemui.dump.DumpManager;
@@ -120,6 +121,8 @@
 import com.android.systemui.power.domain.interactor.PowerInteractor;
 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;
 import com.android.systemui.shade.data.repository.FakeShadeRepository;
 import com.android.systemui.shade.data.repository.ShadeRepository;
@@ -137,6 +140,7 @@
 import com.android.systemui.statusbar.StatusBarStateControllerImpl;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.VibratorHelper;
+import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository;
 import com.android.systemui.statusbar.notification.ConversationNotificationManager;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
@@ -168,6 +172,7 @@
 import com.android.systemui.statusbar.phone.StatusBarTouchableRegionManager;
 import com.android.systemui.statusbar.phone.TapAgainViewController;
 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository;
 import com.android.systemui.statusbar.policy.CastController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
@@ -176,8 +181,10 @@
 import com.android.systemui.statusbar.policy.KeyguardUserSwitcherController;
 import com.android.systemui.statusbar.policy.KeyguardUserSwitcherView;
 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController;
+import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository;
 import com.android.systemui.statusbar.window.StatusBarWindowStateController;
 import com.android.systemui.unfold.SysUIUnfoldComponent;
+import com.android.systemui.user.domain.interactor.UserSwitcherInteractor;
 import com.android.systemui.util.kotlin.JavaAdapter;
 import com.android.systemui.util.time.FakeSystemClock;
 import com.android.systemui.util.time.SystemClock;
@@ -197,6 +204,8 @@
 import java.util.Optional;
 
 import kotlinx.coroutines.CoroutineDispatcher;
+import kotlinx.coroutines.flow.StateFlowKt;
+import kotlinx.coroutines.test.TestScope;
 
 public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
 
@@ -324,7 +333,6 @@
             mEmptySpaceClickListenerCaptor;
     @Mock protected ActivityStarter mActivityStarter;
     @Mock protected KeyguardFaceAuthInteractor mKeyguardFaceAuthInteractor;
-    @Mock private ShadeInteractor mShadeInteractor;
     @Mock private JavaAdapter mJavaAdapter;
     @Mock private CastController mCastController;
     @Mock private KeyguardRootView mKeyguardRootView;
@@ -335,6 +343,9 @@
     protected KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor;
     protected FakeKeyguardRepository mFakeKeyguardRepository;
     protected KeyguardInteractor mKeyguardInteractor;
+    protected SceneTestUtils mUtils = new SceneTestUtils(this);
+    protected TestScope mTestScope = mUtils.getTestScope();
+    protected ShadeInteractor mShadeInteractor;
     protected PowerInteractor mPowerInteractor;
     protected NotificationPanelViewController.TouchHandler mTouchHandler;
     protected ConfigurationController mConfigurationController;
@@ -370,10 +381,31 @@
         mKeyguardInteractor = keyguardInteractorDeps.getKeyguardInteractor();
         mShadeRepository = new FakeShadeRepository();
         mPowerInteractor = keyguardInteractorDeps.getPowerInteractor();
+        when(mKeyguardTransitionInteractor.isInTransitionToStateWhere(any())).thenReturn(
+                StateFlowKt.MutableStateFlow(false));
+        mShadeInteractor = new ShadeInteractor(
+                mTestScope.getBackgroundScope(),
+                new FakeDeviceProvisioningRepository(),
+                new FakeDisableFlagsRepository(),
+                mDozeParameters,
+                new FakeSceneContainerFlags(),
+                mUtils::sceneInteractor,
+                mFakeKeyguardRepository,
+                mKeyguardTransitionInteractor,
+                mPowerInteractor,
+                new FakeUserSetupRepository(),
+                mock(UserSwitcherInteractor.class),
+                new SharedNotificationContainerInteractor(
+                        new FakeConfigurationRepository(),
+                        mContext,
+                        new ResourcesSplitShadeStateController()
+                ),
+                mShadeRepository
+        );
 
         SystemClock systemClock = new FakeSystemClock();
-        mStatusBarStateController = new StatusBarStateControllerImpl(mUiEventLogger, mDumpManager,
-                mInteractionJankMonitor, mShadeExpansionStateManager);
+        mStatusBarStateController = new StatusBarStateControllerImpl(mUiEventLogger,
+                mInteractionJankMonitor, mJavaAdapter, () -> mShadeInteractor);
 
         KeyguardStatusView keyguardStatusView = new KeyguardStatusView(mContext);
         keyguardStatusView.setId(R.id.keyguard_status_view);
@@ -532,8 +564,9 @@
                 new NotificationWakeUpCoordinator(
                         mDumpManager,
                         mock(HeadsUpManager.class),
-                        new StatusBarStateControllerImpl(new UiEventLoggerFake(), mDumpManager,
-                                mInteractionJankMonitor, mShadeExpansionStateManager),
+                        new StatusBarStateControllerImpl(new UiEventLoggerFake(),
+                                mInteractionJankMonitor,
+                                mJavaAdapter, () -> mShadeInteractor),
                         mKeyguardBypassController,
                         mDozeParameters,
                         mScreenOffAnimationController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index be82bc3..4ba850c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -54,7 +54,6 @@
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository;
-import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FakeFeatureFlagsClassic;
 import com.android.systemui.keyguard.KeyguardViewMediator;
@@ -185,7 +184,6 @@
                 powerInteractor,
                 featureFlags,
                 sceneContainerFlags,
-                new FakeDeviceEntryRepository(),
                 new FakeKeyguardBouncerRepository(),
                 configurationRepository,
                 shadeRepository,
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 0fcfaf9..2f45b12 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
@@ -39,7 +39,6 @@
 import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository;
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository;
-import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FakeFeatureFlagsClassic;
 import com.android.systemui.flags.FeatureFlags;
@@ -188,8 +187,8 @@
     public void setup() {
         MockitoAnnotations.initMocks(this);
         when(mPanelViewControllerLazy.get()).thenReturn(mNotificationPanelViewController);
-        mStatusBarStateController = new StatusBarStateControllerImpl(mUiEventLogger, mDumpManager,
-                mInteractionJankMonitor, mShadeExpansionStateManager);
+        mStatusBarStateController = new StatusBarStateControllerImpl(mUiEventLogger,
+                mInteractionJankMonitor, mock(JavaAdapter.class), () -> mShadeInteractor);
 
         FakeDeviceProvisioningRepository deviceProvisioningRepository =
                 new FakeDeviceProvisioningRepository();
@@ -219,7 +218,6 @@
                 powerInteractor,
                 featureFlags,
                 sceneContainerFlags,
-                new FakeDeviceEntryRepository(),
                 new FakeKeyguardBouncerRepository(),
                 configurationRepository,
                 mShadeRepository,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
index 8f06e63..6eabf44 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
@@ -145,22 +145,15 @@
     }
 
     @Test
-    public void testOnRepeatedlyLoadUnload_PluginFreed() {
+    public void testOnUnloadAfterLoad() {
         mPluginInstance.onCreate();
         mPluginInstance.loadPlugin();
+        assertNotNull(mPluginInstance.getPlugin());
         assertInstances(1, 1);
 
         mPluginInstance.unloadPlugin();
         assertNull(mPluginInstance.getPlugin());
         assertInstances(0, 0);
-
-        mPluginInstance.loadPlugin();
-        assertInstances(1, 1);
-
-        mPluginInstance.unloadPlugin();
-        mPluginInstance.onDestroy();
-        assertNull(mPluginInstance.getPlugin());
-        assertInstances(0, 0);
     }
 
     @Test
@@ -169,7 +162,7 @@
         mPluginInstance.onCreate();
         assertEquals(1, mPluginListener.mAttachedCount);
         assertEquals(0, mPluginListener.mLoadCount);
-        assertEquals(null, mPluginInstance.getPlugin());
+        assertNull(mPluginInstance.getPlugin());
         assertInstances(0, 0);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
index e8923a5..970a0f7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
@@ -9,13 +9,18 @@
 import com.android.TestMocksModule
 import com.android.systemui.ExpandHelper
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingCollectorFake
+import com.android.systemui.classifier.FalsingManagerFake
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FakeFeatureFlagsClassicModule
 import com.android.systemui.flags.Flags
 import com.android.systemui.media.controls.ui.MediaHierarchyManager
 import com.android.systemui.plugins.qs.QS
+import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.res.R
 import com.android.systemui.shade.ShadeViewController
+import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
 import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
@@ -26,7 +31,9 @@
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.statusbar.phone.ScrimController
 import com.android.systemui.statusbar.policy.FakeConfigurationController
+import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController
 import com.android.systemui.user.domain.UserDomainLayerModule
+import com.android.systemui.util.mockito.mock
 import dagger.BindsInstance
 import dagger.Component
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -51,6 +58,7 @@
 import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.junit.MockitoJUnit
 
@@ -64,10 +72,8 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 class LockscreenShadeTransitionControllerTest : SysuiTestCase() {
 
+    private lateinit var transitionController: LockscreenShadeTransitionController
     private lateinit var testComponent: TestComponent
-
-    private val transitionController
-        get() = testComponent.transitionController
     private val configurationController
         get() = testComponent.configurationController
     private val disableFlagsRepository
@@ -85,8 +91,11 @@
     @Mock lateinit var mediaHierarchyManager: MediaHierarchyManager
     @Mock lateinit var nsslController: NotificationStackScrollLayoutController
     @Mock lateinit var qS: QS
+    @Mock lateinit var qsTransitionController: LockscreenShadeQsTransitionController
     @Mock lateinit var scrimController: ScrimController
     @Mock lateinit var shadeViewController: ShadeViewController
+    @Mock lateinit var singleShadeOverScroller: SingleShadeLockScreenOverScroller
+    @Mock lateinit var splitShadeOverScroller: SplitShadeLockScreenOverScroller
     @Mock lateinit var stackscroller: NotificationStackScrollLayout
     @Mock lateinit var statusbarStateController: SysuiStatusBarStateController
     @Mock lateinit var transitionControllerCallback: LockscreenShadeTransitionController.Callback
@@ -135,6 +144,49 @@
                         )
                 )
 
+        transitionController =
+            LockscreenShadeTransitionController(
+                statusBarStateController = statusbarStateController,
+                logger = mock(),
+                keyguardBypassController = keyguardBypassController,
+                lockScreenUserManager = lockScreenUserManager,
+                falsingCollector = FalsingCollectorFake(),
+                ambientState = mock(),
+                mediaHierarchyManager = mediaHierarchyManager,
+                scrimTransitionController =
+                    LockscreenShadeScrimTransitionController(
+                        scrimController = scrimController,
+                        context = context,
+                        configurationController = configurationController,
+                        dumpManager = mock(),
+                        splitShadeStateController = ResourcesSplitShadeStateController()
+                    ),
+                keyguardTransitionControllerFactory = { notificationPanelController ->
+                    LockscreenShadeKeyguardTransitionController(
+                        mediaHierarchyManager = mediaHierarchyManager,
+                        notificationPanelController = notificationPanelController,
+                        context = context,
+                        configurationController = configurationController,
+                        dumpManager = mock(),
+                        splitShadeStateController = ResourcesSplitShadeStateController()
+                    )
+                },
+                depthController = depthController,
+                context = context,
+                splitShadeOverScrollerFactory = { _, _ -> splitShadeOverScroller },
+                singleShadeOverScrollerFactory = { singleShadeOverScroller },
+                activityStarter = mock(),
+                wakefulnessLifecycle = mock(),
+                configurationController = configurationController,
+                falsingManager = FalsingManagerFake(),
+                dumpManager = mock(),
+                qsTransitionControllerFactory = { qsTransitionController },
+                shadeRepository = testComponent.shadeRepository,
+                shadeInteractor = testComponent.shadeInteractor,
+                powerInteractor = testComponent.powerInteractor,
+                splitShadeStateController = ResourcesSplitShadeStateController(),
+            )
+
         transitionController.addCallback(transitionControllerCallback)
         transitionController.shadeViewController = shadeViewController
         transitionController.centralSurfaces = centralSurfaces
@@ -259,7 +311,7 @@
         verify(scrimController, never()).setTransitionToFullShadeProgress(anyFloat(), anyFloat())
         verify(transitionControllerCallback, never())
             .setTransitionToFullShadeAmount(anyFloat(), anyBoolean(), anyLong())
-        verify(qS, never()).setTransitionToFullShadeProgress(anyBoolean(), anyFloat(), anyFloat())
+        verify(qsTransitionController, never()).dragDownAmount = anyFloat()
     }
 
     @Test
@@ -270,7 +322,7 @@
         verify(scrimController).setTransitionToFullShadeProgress(anyFloat(), anyFloat())
         verify(transitionControllerCallback)
             .setTransitionToFullShadeAmount(anyFloat(), anyBoolean(), anyLong())
-        verify(qS).setTransitionToFullShadeProgress(eq(true), anyFloat(), anyFloat())
+        verify(qsTransitionController).dragDownAmount = 10f
         verify(depthController).transitionToFullShadeProgress = anyFloat()
     }
 
@@ -473,8 +525,8 @@
 
         transitionController.dragDownAmount = 10f
 
-        verify(nsslController).setOverScrollAmount(0)
-        verify(scrimController, never()).setNotificationsOverScrollAmount(anyInt())
+        verify(singleShadeOverScroller).expansionDragDownAmount = 10f
+        verifyZeroInteractions(splitShadeOverScroller)
     }
 
     @Test
@@ -483,8 +535,8 @@
 
         transitionController.dragDownAmount = 10f
 
-        verify(nsslController).setOverScrollAmount(0)
-        verify(scrimController).setNotificationsOverScrollAmount(0)
+        verify(splitShadeOverScroller).expansionDragDownAmount = 10f
+        verifyZeroInteractions(singleShadeOverScroller)
     }
 
     @Test
@@ -545,10 +597,11 @@
     )
     interface TestComponent {
 
-        val transitionController: LockscreenShadeTransitionController
-
         val configurationController: FakeConfigurationController
         val disableFlagsRepository: FakeDisableFlagsRepository
+        val powerInteractor: PowerInteractor
+        val shadeInteractor: ShadeInteractor
+        val shadeRepository: FakeShadeRepository
         val testScope: TestScope
 
         @Component.Factory
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java
index 2b3fd34..a4c12f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar;
 
+import static com.android.systemui.flags.SetFlagsRuleExtensionsKt.setFlagDefault;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -37,12 +39,11 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.flags.FakeFeatureFlagsClassic;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.PluginManager;
 import com.android.systemui.statusbar.NotificationListener.NotificationHandler;
 import com.android.systemui.statusbar.data.repository.NotificationListenerSettingsRepository;
 import com.android.systemui.statusbar.domain.interactor.SilentNotificationStatusIconsVisibilityInteractor;
+import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.time.FakeSystemClock;
 
@@ -71,12 +72,9 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-
-        FakeFeatureFlagsClassic featureFlags = new FakeFeatureFlagsClassic();
-        featureFlags.setDefault(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR);
+        setFlagDefault(mSetFlagsRule, NotificationIconContainerRefactor.FLAG_NAME);
         mListener = new NotificationListener(
                 mContext,
-                featureFlags,
                 mNotificationManager,
                 new SilentNotificationStatusIconsVisibilityInteractor(
                         new NotificationListenerSettingsRepository()),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
index 764f7b6..560ebc6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
@@ -33,9 +33,9 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.power.domain.interactor.PowerInteractor;
+import com.android.systemui.shade.domain.interactor.ShadeInteractor;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.RemoteInputControllerLogger;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -43,6 +43,7 @@
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.policy.RemoteInputUriController;
+import com.android.systemui.util.kotlin.JavaAdapter;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -87,7 +88,8 @@
                 new RemoteInputControllerLogger(logcatLogBuffer()),
                 mClickNotifier,
                 new ActionClickLogger(logcatLogBuffer()),
-                mock(DumpManager.class));
+                mock(JavaAdapter.class),
+                mock(ShadeInteractor.class));
         mEntry = new NotificationEntryBuilder()
                 .setPkg(TEST_PACKAGE_NAME)
                 .setOpPkg(TEST_PACKAGE_NAME)
@@ -145,7 +147,8 @@
                 RemoteInputControllerLogger remoteInputControllerLogger,
                 NotificationClickNotifier clickNotifier,
                 ActionClickLogger actionClickLogger,
-                DumpManager dumpManager) {
+                JavaAdapter javaAdapter,
+                ShadeInteractor shadeInteractor) {
             super(
                     context,
                     notifPipelineFlags,
@@ -158,7 +161,8 @@
                     remoteInputControllerLogger,
                     clickNotifier,
                     actionClickLogger,
-                    dumpManager);
+                    javaAdapter,
+                    shadeInteractor);
         }
 
         public void setUpWithPresenterForTest(Callback callback,
@@ -170,3 +174,4 @@
 
     }
 }
+
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
index 3327e42..d6dfc5e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
@@ -23,9 +23,30 @@
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.dump.DumpManager
+import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.classifier.FalsingCollectorFake
+import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
+import com.android.systemui.flags.FakeFeatureFlagsClassic
+import com.android.systemui.keyguard.data.repository.FakeCommandQueue
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.shade.ShadeExpansionStateManager
+import com.android.systemui.power.data.repository.FakePowerRepository
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.scene.SceneTestUtils
+import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags
+import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository
+import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository
+import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController
+import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository
+import com.android.systemui.util.mockito.mock
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
@@ -40,17 +61,22 @@
 import org.mockito.Mockito
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
+import org.mockito.Mockito.`when` as whenever
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper
 class StatusBarStateControllerImplTest : SysuiTestCase() {
 
+    private val utils = SceneTestUtils(this)
+    private val testScope = utils.testScope
+    private lateinit var shadeInteractor: ShadeInteractor
+    private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor
+    private lateinit var fromPrimaryBouncerTransitionInteractor:
+        FromPrimaryBouncerTransitionInteractor
     @Mock lateinit var interactionJankMonitor: InteractionJankMonitor
-    @Mock private lateinit var mockDarkAnimator: ObjectAnimator
-    @Mock private lateinit var shadeExpansionStateManager: ShadeExpansionStateManager
+    @Mock lateinit var mockDarkAnimator: ObjectAnimator
 
     private lateinit var controller: StatusBarStateControllerImpl
     private lateinit var uiEventLogger: UiEventLoggerFake
@@ -64,11 +90,75 @@
         uiEventLogger = UiEventLoggerFake()
         controller = object : StatusBarStateControllerImpl(
             uiEventLogger,
-            mock(DumpManager::class.java),
-            interactionJankMonitor, shadeExpansionStateManager
+            interactionJankMonitor,
+            mock(),
+            { shadeInteractor }
         ) {
             override fun createDarkAnimator(): ObjectAnimator { return mockDarkAnimator }
         }
+
+        val powerInteractor = PowerInteractor(
+            FakePowerRepository(),
+            FalsingCollectorFake(),
+            mock(),
+            controller)
+        val keyguardRepository = FakeKeyguardRepository()
+        val keyguardTransitionRepository = FakeKeyguardTransitionRepository()
+        val featureFlags = FakeFeatureFlagsClassic()
+        val shadeRepository = FakeShadeRepository()
+        val sceneContainerFlags = FakeSceneContainerFlags()
+        val configurationRepository = FakeConfigurationRepository()
+        val keyguardInteractor = KeyguardInteractor(
+            keyguardRepository,
+            FakeCommandQueue(),
+            powerInteractor,
+            featureFlags,
+            sceneContainerFlags,
+            FakeKeyguardBouncerRepository(),
+            configurationRepository,
+            shadeRepository,
+            utils::sceneInteractor)
+        val keyguardTransitionInteractor = KeyguardTransitionInteractor(
+            testScope.backgroundScope,
+            keyguardTransitionRepository,
+            { keyguardInteractor },
+            { fromLockscreenTransitionInteractor },
+            { fromPrimaryBouncerTransitionInteractor })
+        fromLockscreenTransitionInteractor = FromLockscreenTransitionInteractor(
+            keyguardTransitionRepository,
+            keyguardTransitionInteractor,
+            testScope.backgroundScope,
+            keyguardInteractor,
+            featureFlags,
+            shadeRepository,
+            powerInteractor)
+        fromPrimaryBouncerTransitionInteractor = FromPrimaryBouncerTransitionInteractor(
+            keyguardTransitionRepository,
+            keyguardTransitionInteractor,
+            testScope.backgroundScope,
+            keyguardInteractor,
+            featureFlags,
+            mock(),
+            mock(),
+            powerInteractor)
+        shadeInteractor = ShadeInteractor(
+            testScope.backgroundScope,
+            FakeDeviceProvisioningRepository(),
+            FakeDisableFlagsRepository(),
+            mock(),
+            sceneContainerFlags,
+            utils::sceneInteractor,
+            keyguardRepository,
+            keyguardTransitionInteractor,
+            powerInteractor,
+            FakeUserSetupRepository(),
+            mock(),
+            SharedNotificationContainerInteractor(
+                configurationRepository,
+                mContext,
+                ResourcesSplitShadeStateController()),
+            shadeRepository,
+        )
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/OWNERS b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/OWNERS
new file mode 100644
index 0000000..7f5384d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/OWNERS
@@ -0,0 +1,3 @@
+set noparent
+
+include /packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
index a736182..e81207e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
@@ -19,8 +19,7 @@
 import android.testing.TestableLooper.RunWithLooper
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.FakeFeatureFlagsClassic
-import com.android.systemui.flags.Flags
+import com.android.systemui.flags.setFlagValue
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
@@ -30,6 +29,7 @@
 import com.android.systemui.statusbar.notification.collection.render.NotifStackController
 import com.android.systemui.statusbar.notification.collection.render.NotifStats
 import com.android.systemui.statusbar.notification.domain.interactor.RenderNotificationListInteractor
+import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
 import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING
 import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT
 import com.android.systemui.statusbar.phone.NotificationIconAreaController
@@ -39,6 +39,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
+import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations.initMocks
@@ -59,15 +60,18 @@
     @Mock private lateinit var stackController: NotifStackController
     @Mock private lateinit var section: NotifSection
 
-    val featureFlags =
-        FakeFeatureFlagsClassic().apply { setDefault(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR) }
-
     @Before
     fun setUp() {
         initMocks(this)
+        setUp(NotificationIconContainerRefactor.FLAG_NAME to null)
+        entry = NotificationEntryBuilder().setSection(section).build()
+    }
+
+    private fun setUp(vararg flags: Pair<String, Boolean?>) {
+        flags.forEach { (name, value) -> mSetFlagsRule.setFlagValue(name, value) }
+        reset(pipeline)
         coordinator =
             StackCoordinator(
-                featureFlags,
                 groupExpansionManagerImpl,
                 notificationIconAreaController,
                 renderListInteractor,
@@ -76,19 +80,18 @@
         afterRenderListListener = withArgCaptor {
             verify(pipeline).addOnAfterRenderListListener(capture())
         }
-        entry = NotificationEntryBuilder().setSection(section).build()
     }
 
     @Test
     fun testUpdateNotificationIcons() {
-        featureFlags.set(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR, false)
+        setUp(NotificationIconContainerRefactor.FLAG_NAME to false)
         afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
         verify(notificationIconAreaController).updateNotificationIcons(eq(listOf(entry)))
     }
 
     @Test
     fun testSetRenderedListOnInteractor() {
-        featureFlags.set(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR, true)
+        setUp(NotificationIconContainerRefactor.FLAG_NAME to true)
         afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
         verify(renderListInteractor).setRenderedList(eq(listOf(entry)))
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
index 390c1dd..02a67d0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
@@ -21,23 +21,25 @@
 import android.os.PowerManager
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
+import com.android.SysUITestModule
+import com.android.TestMocksModule
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.accessibility.data.repository.FakeAccessibilityRepository
-import com.android.systemui.accessibility.domain.interactor.AccessibilityInteractor
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.power.data.repository.FakePowerRepository
-import com.android.systemui.power.domain.interactor.PowerInteractorFactory
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
-import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModel
-import com.android.systemui.statusbar.notification.shelf.domain.interactor.NotificationShelfInteractor
+import com.android.systemui.statusbar.SysuiStatusBarStateController
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModelModule
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Rule
@@ -55,98 +57,118 @@
 
     @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule()
 
-    // mocks
     @Mock private lateinit var keyguardTransitionController: LockscreenShadeTransitionController
     @Mock private lateinit var screenOffAnimationController: ScreenOffAnimationController
-    @Mock private lateinit var statusBarStateController: StatusBarStateController
+    @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
 
-    // fakes
-    private val keyguardRepository = FakeKeyguardRepository()
-    private val deviceEntryFaceAuthRepository = FakeDeviceEntryFaceAuthRepository()
-    private val a11yRepo = FakeAccessibilityRepository()
-    private val powerRepository = FakePowerRepository()
-    private val powerInteractor by lazy {
-        PowerInteractorFactory.create(
-                repository = powerRepository,
-                screenOffAnimationController = screenOffAnimationController,
-                statusBarStateController = statusBarStateController,
-            )
-            .powerInteractor
-    }
-
-    // real impls
-    private val a11yInteractor = AccessibilityInteractor(a11yRepo)
-    private val activatableViewModel = ActivatableNotificationViewModel(a11yInteractor)
-    private val interactor by lazy {
-        NotificationShelfInteractor(
-            keyguardRepository,
-            deviceEntryFaceAuthRepository,
-            powerInteractor,
-            keyguardTransitionController,
-        )
-    }
-    private val underTest by lazy { NotificationShelfViewModel(interactor, activatableViewModel) }
+    private lateinit var testComponent: TestComponent
 
     @Before
     fun setUp() {
         whenever(screenOffAnimationController.allowWakeUpIfDozing()).thenReturn(true)
+        testComponent =
+            DaggerNotificationShelfViewModelTest_TestComponent.factory()
+                .create(
+                    test = this,
+                    mocks =
+                        TestMocksModule(
+                            lockscreenShadeTransitionController = keyguardTransitionController,
+                            screenOffAnimationController = screenOffAnimationController,
+                            statusBarStateController = statusBarStateController,
+                        )
+                )
     }
 
     @Test
-    fun canModifyColorOfNotifications_whenKeyguardNotShowing() = runTest {
-        val canModifyNotifColor by collectLastValue(underTest.canModifyColorOfNotifications)
+    fun canModifyColorOfNotifications_whenKeyguardNotShowing() =
+        with(testComponent) {
+            testScope.runTest {
+                val canModifyNotifColor by collectLastValue(underTest.canModifyColorOfNotifications)
 
-        keyguardRepository.setKeyguardShowing(false)
+                keyguardRepository.setKeyguardShowing(false)
 
-        assertThat(canModifyNotifColor).isTrue()
-    }
+                assertThat(canModifyNotifColor).isTrue()
+            }
+        }
 
     @Test
-    fun canModifyColorOfNotifications_whenKeyguardShowingAndNotBypass() = runTest {
-        val canModifyNotifColor by collectLastValue(underTest.canModifyColorOfNotifications)
+    fun canModifyColorOfNotifications_whenKeyguardShowingAndNotBypass() =
+        with(testComponent) {
+            testScope.runTest {
+                val canModifyNotifColor by collectLastValue(underTest.canModifyColorOfNotifications)
 
-        keyguardRepository.setKeyguardShowing(true)
-        deviceEntryFaceAuthRepository.isBypassEnabled.value = false
+                keyguardRepository.setKeyguardShowing(true)
+                deviceEntryFaceAuthRepository.isBypassEnabled.value = false
 
-        assertThat(canModifyNotifColor).isTrue()
-    }
+                assertThat(canModifyNotifColor).isTrue()
+            }
+        }
 
     @Test
-    fun cannotModifyColorOfNotifications_whenBypass() = runTest {
-        val canModifyNotifColor by collectLastValue(underTest.canModifyColorOfNotifications)
+    fun cannotModifyColorOfNotifications_whenBypass() =
+        with(testComponent) {
+            testScope.runTest {
+                val canModifyNotifColor by collectLastValue(underTest.canModifyColorOfNotifications)
 
-        keyguardRepository.setKeyguardShowing(true)
-        deviceEntryFaceAuthRepository.isBypassEnabled.value = true
+                keyguardRepository.setKeyguardShowing(true)
+                deviceEntryFaceAuthRepository.isBypassEnabled.value = true
 
-        assertThat(canModifyNotifColor).isFalse()
-    }
+                assertThat(canModifyNotifColor).isFalse()
+            }
+        }
 
     @Test
-    fun isClickable_whenKeyguardShowing() = runTest {
-        val isClickable by collectLastValue(underTest.isClickable)
+    fun isClickable_whenKeyguardShowing() =
+        with(testComponent) {
+            testScope.runTest {
+                val isClickable by collectLastValue(underTest.isClickable)
 
-        keyguardRepository.setKeyguardShowing(true)
+                keyguardRepository.setKeyguardShowing(true)
 
-        assertThat(isClickable).isTrue()
-    }
+                assertThat(isClickable).isTrue()
+            }
+        }
 
     @Test
-    fun isNotClickable_whenKeyguardNotShowing() = runTest {
-        val isClickable by collectLastValue(underTest.isClickable)
+    fun isNotClickable_whenKeyguardNotShowing() =
+        with(testComponent) {
+            testScope.runTest {
+                val isClickable by collectLastValue(underTest.isClickable)
 
-        keyguardRepository.setKeyguardShowing(false)
+                keyguardRepository.setKeyguardShowing(false)
 
-        assertThat(isClickable).isFalse()
-    }
+                assertThat(isClickable).isFalse()
+            }
+        }
 
     @Test
-    fun onClicked_goesToLockedShade() {
-        whenever(statusBarStateController.isDozing).thenReturn(true)
+    fun onClicked_goesToLockedShade() =
+        with(testComponent) {
+            whenever(statusBarStateController.isDozing).thenReturn(true)
 
-        underTest.onShelfClicked()
+            underTest.onShelfClicked()
 
-        assertThat(powerRepository.lastWakeReason).isNotNull()
-        assertThat(powerRepository.lastWakeReason).isEqualTo(PowerManager.WAKE_REASON_GESTURE)
-        verify(keyguardTransitionController).goToLockedShade(Mockito.isNull(), eq(true))
+            assertThat(powerRepository.lastWakeReason).isNotNull()
+            assertThat(powerRepository.lastWakeReason).isEqualTo(PowerManager.WAKE_REASON_GESTURE)
+            verify(keyguardTransitionController).goToLockedShade(Mockito.isNull(), eq(true))
+        }
+
+    @Component(modules = [SysUITestModule::class, ActivatableNotificationViewModelModule::class])
+    @SysUISingleton
+    interface TestComponent {
+
+        val underTest: NotificationShelfViewModel
+        val deviceEntryFaceAuthRepository: FakeDeviceEntryFaceAuthRepository
+        val keyguardRepository: FakeKeyguardRepository
+        val powerRepository: FakePowerRepository
+        val testScope: TestScope
+
+        @Component.Factory
+        interface Factory {
+            fun create(
+                @BindsInstance test: SysuiTestCase,
+                mocks: TestMocksModule,
+            ): TestComponent
+        }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index 3dafb23..39e3d5da3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -53,6 +53,7 @@
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.classifier.FalsingManagerFake;
+import com.android.systemui.common.ui.ConfigurationState;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.flags.Flags;
@@ -84,6 +85,7 @@
 import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController;
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository;
 import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor;
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ShelfNotificationIconViewStore;
 import com.android.systemui.statusbar.notification.init.NotificationsController;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
@@ -716,8 +718,9 @@
                 mSecureSettings,
                 mock(NotificationDismissibilityProvider.class),
                 mActivityStarter,
-                new ResourcesSplitShadeStateController()
-        );
+                new ResourcesSplitShadeStateController(),
+                mock(ConfigurationState.class),
+                mock(ShelfNotificationIconViewStore.class));
     }
 
     static class LogMatcher implements ArgumentMatcher<LogMaker> {
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 41eaf85..6478a3e 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
@@ -21,6 +21,7 @@
 import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN;
 import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
 
+import static com.android.systemui.flags.SetFlagsRuleExtensionsKt.setFlagDefault;
 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
 import static com.android.systemui.statusbar.StatusBarState.SHADE;
 
@@ -163,6 +164,7 @@
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptLogger;
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl;
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
+import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
 import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment;
@@ -185,8 +187,6 @@
 import com.android.wm.shell.bubbles.Bubbles;
 import com.android.wm.shell.startingsurface.StartingSurface;
 
-import dagger.Lazy;
-
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -200,6 +200,8 @@
 
 import javax.inject.Provider;
 
+import dagger.Lazy;
+
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper(setAsMainLooper = true)
@@ -335,7 +337,7 @@
         mFeatureFlags.set(Flags.WM_ENABLE_PREDICTIVE_BACK_SYSUI, false);
         // Set default value to avoid IllegalStateException.
         mFeatureFlags.set(Flags.SHORTCUT_LIST_SEARCH_LAYOUT, false);
-        mFeatureFlags.setDefault(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR);
+        setFlagDefault(mSetFlagsRule, NotificationIconContainerRefactor.FLAG_NAME);
         // For the Shade to respond to Back gesture, we must enable the event routing
         mFeatureFlags.set(Flags.WM_SHADE_ALLOW_BACK_GESTURE, true);
         // For the Shade to animate during the Back gesture, we must enable the animation flag.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
index 472709c..19215e3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.phone;
 
+import static com.android.systemui.flags.SetFlagsRuleExtensionsKt.setFlagDefault;
+
 import static org.junit.Assert.assertFalse;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -43,7 +45,6 @@
 import com.android.systemui.doze.DozeHost;
 import com.android.systemui.doze.DozeLog;
 import com.android.systemui.flags.FakeFeatureFlagsClassic;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.keyguard.domain.interactor.DozeInteractor;
 import com.android.systemui.shade.NotificationShadeWindowViewController;
@@ -53,6 +54,7 @@
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.StatusBarStateControllerImpl;
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
+import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
@@ -105,7 +107,7 @@
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-        mFeatureFlags.setDefault(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR);
+        setFlagDefault(mSetFlagsRule, NotificationIconContainerRefactor.FLAG_NAME);
         mDozeServiceHost = new DozeServiceHost(mDozeLog, mPowerManager, mWakefullnessLifecycle,
                 mStatusBarStateController, mDeviceProvisionedController, mFeatureFlags,
                 mHeadsUpManager, mBatteryController, mScrimController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java
index 529e2c9..1fad2a2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.phone;
 
+import static com.android.systemui.flags.SetFlagsRuleExtensionsKt.setFlagDefault;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -35,7 +37,6 @@
 
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.flags.FakeFeatureFlagsClassic;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shade.ShadeHeadsUpTracker;
@@ -47,6 +48,7 @@
 import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationIconInteractor;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
+import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor;
 import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
 import com.android.systemui.statusbar.policy.Clock;
@@ -90,7 +92,7 @@
     @Before
     public void setUp() throws Exception {
         allowTestableLooperAsMainThread();
-        mFeatureFlags.setDefault(Flags.NOTIFICATION_ICON_CONTAINER_REFACTOR);
+        setFlagDefault(mSetFlagsRule, NotificationIconContainerRefactor.FLAG_NAME);
         mTestHelper = new NotificationTestHelper(
                 mContext,
                 mDependency,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java
index cda2a74..48b95d4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java
@@ -34,7 +34,7 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.res.R;
-import com.android.systemui.shade.ShadeExpansionStateManager;
+import com.android.systemui.shade.domain.interactor.ShadeInteractor;
 import com.android.systemui.statusbar.AlertingNotificationManager;
 import com.android.systemui.statusbar.AlertingNotificationManagerTest;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
@@ -45,6 +45,7 @@
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.HeadsUpManagerLogger;
+import com.android.systemui.util.kotlin.JavaAdapter;
 
 import org.junit.After;
 import org.junit.Before;
@@ -56,6 +57,8 @@
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
+import kotlinx.coroutines.flow.StateFlowKt;
+
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
@@ -70,8 +73,9 @@
     @Mock private KeyguardBypassController mBypassController;
     @Mock private ConfigurationControllerImpl mConfigurationController;
     @Mock private AccessibilityManagerWrapper mAccessibilityManagerWrapper;
-    @Mock private ShadeExpansionStateManager mShadeExpansionStateManager;
     @Mock private UiEventLogger mUiEventLogger;
+    @Mock private JavaAdapter mJavaAdapter;
+    @Mock private ShadeInteractor mShadeInteractor;
 
     private static final class TestableHeadsUpManagerPhone extends HeadsUpManagerPhone {
         TestableHeadsUpManagerPhone(
@@ -85,7 +89,8 @@
                 Handler handler,
                 AccessibilityManagerWrapper accessibilityManagerWrapper,
                 UiEventLogger uiEventLogger,
-                ShadeExpansionStateManager shadeExpansionStateManager
+                JavaAdapter javaAdapter,
+                ShadeInteractor shadeInteractor
         ) {
             super(
                     context,
@@ -98,7 +103,8 @@
                     handler,
                     accessibilityManagerWrapper,
                     uiEventLogger,
-                    shadeExpansionStateManager
+                    javaAdapter,
+                    shadeInteractor
             );
             mMinimumDisplayTime = TEST_MINIMUM_DISPLAY_TIME;
             mAutoDismissNotificationDecay = TEST_AUTO_DISMISS_TIME;
@@ -117,7 +123,8 @@
                 mTestHandler,
                 mAccessibilityManagerWrapper,
                 mUiEventLogger,
-                mShadeExpansionStateManager
+                mJavaAdapter,
+                mShadeInteractor
         );
     }
 
@@ -129,6 +136,7 @@
     @Before
     @Override
     public void setUp() {
+        when(mShadeInteractor.isAnyExpanded()).thenReturn(StateFlowKt.MutableStateFlow(false));
         final AccessibilityManagerWrapper accessibilityMgr =
                 mDependency.injectMockDependency(AccessibilityManagerWrapper.class);
         when(accessibilityMgr.getRecommendedTimeoutMillis(anyInt(), anyInt()))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
index 6484389..361df1c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
@@ -168,7 +168,6 @@
                 PowerInteractorFactory.create().getPowerInteractor(),
                 mFeatureFlags,
                 mSceneTestUtils.getSceneContainerFlags(),
-                mSceneTestUtils.getDeviceEntryRepository(),
                 new FakeKeyguardBouncerRepository(),
                 new FakeConfigurationRepository(),
                 new FakeShadeRepository(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java
index 1b8cfd4..92e40df 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java
@@ -15,6 +15,8 @@
  */
 package com.android.systemui.statusbar.phone;
 
+import static com.android.systemui.flags.SetFlagsRuleExtensionsKt.setFlagDefault;
+
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.verify;
@@ -28,12 +30,14 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.SetFlagsRuleExtensionsKt;
 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.shared.NotificationIconContainerRefactor;
 import com.android.systemui.statusbar.window.StatusBarWindowController;
 import com.android.wm.shell.bubbles.Bubbles;
 
@@ -82,6 +86,7 @@
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
+        mSetFlagsRule.disableFlags(NotificationIconContainerRefactor.FLAG_NAME);
         mController = new LegacyNotificationIconAreaControllerImpl(
                 mContext,
                 mStatusBarStateController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconContainerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconContainerTest.kt
index c282c1e..2b28562 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconContainerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconContainerTest.kt
@@ -20,12 +20,15 @@
 import android.testing.TestableLooper.RunWithLooper
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.setFlagDefault
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT
 import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
+import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
 import junit.framework.Assert.assertEquals
 import junit.framework.Assert.assertFalse
 import junit.framework.Assert.assertTrue
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito.mock
@@ -41,6 +44,11 @@
 
     private val iconContainer = NotificationIconContainer(context, /* attrs= */ null)
 
+    @Before
+    fun setup() {
+        mSetFlagsRule.setFlagDefault(NotificationIconContainerRefactor.FLAG_NAME)
+    }
+
     @Test
     fun calculateWidthFor_zeroIcons_widthIsZero() {
         assertEquals(/* expected= */ iconContainer.calculateWidthFor(/* numIcons= */ 0f),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index 9a77f0c..d1b9b8a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -24,13 +24,8 @@
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyFloat;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.Fragment;
@@ -42,8 +37,6 @@
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper.RunWithLooper;
 import android.view.View;
-import android.view.ViewPropertyAnimator;
-import android.widget.FrameLayout;
 
 import androidx.test.filters.SmallTest;
 
@@ -67,10 +60,8 @@
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarNotificationIconViewStore;
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel;
-import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
-import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
 import com.android.systemui.statusbar.phone.StatusBarHideIconsForBouncerManager;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.statusbar.phone.StatusBarLocationPublisher;
@@ -283,15 +274,15 @@
 
         fragment.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_NOTIFICATION_ICONS, 0, false);
 
-        verify(mNotificationAreaInner, atLeast(1)).setVisibility(eq(View.INVISIBLE));
+        assertEquals(View.INVISIBLE, mNotificationAreaInner.getVisibility());
 
         fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
 
-        verify(mNotificationAreaInner, atLeast(1)).setVisibility(eq(View.VISIBLE));
+        assertEquals(View.VISIBLE, mNotificationAreaInner.getVisibility());
 
         fragment.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_NOTIFICATION_ICONS, 0, false);
 
-        verify(mNotificationAreaInner, atLeast(1)).setVisibility(eq(View.INVISIBLE));
+        assertEquals(View.INVISIBLE, mNotificationAreaInner.getVisibility());
     }
 
     @Test
@@ -323,7 +314,7 @@
 
         // THEN all views are hidden
         assertEquals(View.INVISIBLE, getClockView().getVisibility());
-        verify(mNotificationAreaInner, atLeast(1)).setVisibility(eq(View.INVISIBLE));
+        assertEquals(View.INVISIBLE, mNotificationAreaInner.getVisibility());
         assertEquals(View.INVISIBLE, getEndSideContentView().getVisibility());
     }
 
@@ -339,7 +330,7 @@
 
         // THEN all views are shown
         assertEquals(View.VISIBLE, getClockView().getVisibility());
-        verify(mNotificationAreaInner, atLeast(1)).setVisibility(eq(View.VISIBLE));
+        assertEquals(View.VISIBLE, mNotificationAreaInner.getVisibility());
         assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
     }
 
@@ -356,7 +347,7 @@
 
         // THEN all views are hidden
         assertEquals(View.INVISIBLE, getClockView().getVisibility());
-        verify(mNotificationAreaInner, atLeast(1)).setVisibility(eq(View.INVISIBLE));
+        assertEquals(View.INVISIBLE, mNotificationAreaInner.getVisibility());
         assertEquals(View.INVISIBLE, getEndSideContentView().getVisibility());
 
         // WHEN the shade is updated to no longer be open
@@ -367,7 +358,7 @@
 
         // THEN all views are shown
         assertEquals(View.VISIBLE, getClockView().getVisibility());
-        verify(mNotificationAreaInner, atLeast(1)).setVisibility(eq(View.VISIBLE));
+        assertEquals(View.VISIBLE, mNotificationAreaInner.getVisibility());
         assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
     }
 
@@ -381,7 +372,7 @@
 
         // THEN all views are shown
         assertEquals(View.VISIBLE, getClockView().getVisibility());
-        verify(mNotificationAreaInner, atLeast(1)).setVisibility(eq(View.VISIBLE));
+        assertEquals(View.VISIBLE, mNotificationAreaInner.getVisibility());
         assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
     }
 
@@ -395,7 +386,7 @@
 
         // THEN all views are hidden
         assertEquals(View.GONE, getClockView().getVisibility());
-        verify(mNotificationAreaInner, atLeast(1)).setVisibility(eq(View.INVISIBLE));
+        assertEquals(View.INVISIBLE, mNotificationAreaInner.getVisibility());
         assertEquals(View.INVISIBLE, getEndSideContentView().getVisibility());
     }
 
@@ -409,7 +400,7 @@
 
         // THEN all views are hidden
         assertEquals(View.GONE, getClockView().getVisibility());
-        verify(mNotificationAreaInner, atLeast(1)).setVisibility(eq(View.INVISIBLE));
+        assertEquals(View.INVISIBLE, mNotificationAreaInner.getVisibility());
         assertEquals(View.INVISIBLE, getEndSideContentView().getVisibility());
 
         // WHEN the transition has finished
@@ -418,7 +409,7 @@
 
         // THEN all views are shown
         assertEquals(View.VISIBLE, getClockView().getVisibility());
-        verify(mNotificationAreaInner, atLeast(1)).setVisibility(eq(View.VISIBLE));
+        assertEquals(View.VISIBLE, mNotificationAreaInner.getVisibility());
         assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
     }
 
@@ -451,7 +442,7 @@
 
         assertEquals(View.VISIBLE,
                 mFragment.getView().findViewById(R.id.ongoing_call_chip).getVisibility());
-        verify(mNotificationAreaInner, atLeast(1)).setVisibility(eq(View.INVISIBLE));
+        assertEquals(View.INVISIBLE, mNotificationAreaInner.getVisibility());
     }
 
     @Test
@@ -507,6 +498,20 @@
     }
 
     @Test
+    public void disable_hasOngoingCall_hidesNotifsWithoutAnimation() {
+        CollapsedStatusBarFragment fragment = resumeAndGetFragment();
+        fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
+
+        // Ongoing call started
+        when(mOngoingCallController.hasOngoingCall()).thenReturn(true);
+        fragment.disable(DEFAULT_DISPLAY, 0, 0, true);
+
+        // Notification area is hidden without delay
+        assertEquals(0f, mNotificationAreaInner.getAlpha(), 0.01);
+        assertEquals(View.INVISIBLE, mNotificationAreaInner.getVisibility());
+    }
+
+    @Test
     public void disable_isDozing_clockAndSystemInfoVisible() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
         when(mStatusBarStateController.isDozing()).thenReturn(true);
@@ -713,8 +718,6 @@
                 mock(NotificationIconContainerStatusBarViewModel.class),
                 mock(ConfigurationState.class),
                 mock(ConfigurationController.class),
-                mock(DozeParameters.class),
-                mock(ScreenOffAnimationController.class),
                 mock(StatusBarNotificationIconViewStore.class),
                 mock(DemoModeController.class));
     }
@@ -729,18 +732,7 @@
     private void setUpNotificationIconAreaController() {
         mMockNotificationAreaController = mock(NotificationIconAreaController.class);
 
-        mNotificationAreaInner = mock(View.class);
-
-        when(mNotificationAreaInner.getLayoutParams()).thenReturn(
-                new FrameLayout.LayoutParams(100, 100));
-        // We should probably start using a real view so that we don't need to mock these methods.
-        ViewPropertyAnimator viewPropertyAnimator = mock(ViewPropertyAnimator.class);
-        when(mNotificationAreaInner.animate()).thenReturn(viewPropertyAnimator);
-        when(viewPropertyAnimator.alpha(anyFloat())).thenReturn(viewPropertyAnimator);
-        when(viewPropertyAnimator.setDuration(anyLong())).thenReturn(viewPropertyAnimator);
-        when(viewPropertyAnimator.setInterpolator(any())).thenReturn(viewPropertyAnimator);
-        when(viewPropertyAnimator.setStartDelay(anyLong())).thenReturn(viewPropertyAnimator);
-        when(viewPropertyAnimator.withEndAction(any())).thenReturn(viewPropertyAnimator);
+        mNotificationAreaInner = new View(mContext);
 
         when(mMockNotificationAreaController.getNotificationInnerAreaView()).thenReturn(
                 mNotificationAreaInner);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/VariableDateViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/VariableDateViewControllerTest.kt
index b78e839..63de068 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/VariableDateViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/VariableDateViewControllerTest.kt
@@ -23,26 +23,27 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.shade.ShadeExpansionStateManager
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.MutableStateFlow
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
 import org.mockito.Captor
 import org.mockito.Mock
-import org.mockito.Mockito.`when`
 import org.mockito.Mockito.anyString
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
 import java.util.Date
 
 @RunWith(AndroidTestingRunner::class)
-@TestableLooper.RunWithLooper
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
 @SmallTest
 class VariableDateViewControllerTest : SysuiTestCase() {
 
@@ -57,12 +58,15 @@
     private lateinit var broadcastDispatcher: BroadcastDispatcher
     @Mock
     private lateinit var view: VariableDateView
+    @Mock
+    private lateinit var shadeInteractor: ShadeInteractor
     @Captor
     private lateinit var onMeasureListenerCaptor: ArgumentCaptor<VariableDateView.OnMeasureListener>
 
+    private val qsExpansion = MutableStateFlow(0F)
+
     private var lastText: String? = null
 
-    private lateinit var shadeExpansionStateManager: ShadeExpansionStateManager
     private lateinit var systemClock: FakeSystemClock
     private lateinit var testableLooper: TestableLooper
     private lateinit var testableHandler: Handler
@@ -80,7 +84,7 @@
         systemClock = FakeSystemClock()
         systemClock.setCurrentTimeMillis(TIME_STAMP)
 
-        shadeExpansionStateManager = ShadeExpansionStateManager()
+        `when`(shadeInteractor.qsExpansion).thenReturn(qsExpansion)
 
         `when`(view.longerPattern).thenReturn(LONG_PATTERN)
         `when`(view.shorterPattern).thenReturn(SHORT_PATTERN)
@@ -91,6 +95,7 @@
             Unit
         }
         `when`(view.isAttachedToWindow).thenReturn(true)
+        `when`(view.viewTreeObserver).thenReturn(mock())
 
         val date = Date(TIME_STAMP)
         longText = getTextForFormat(date, getFormatFromPattern(LONG_PATTERN))
@@ -103,12 +108,12 @@
         }
 
         controller = VariableDateViewController(
-                systemClock,
-                broadcastDispatcher,
-                shadeExpansionStateManager,
-                mock(),
-                testableHandler,
-                view
+            systemClock,
+            broadcastDispatcher,
+            shadeInteractor,
+            mock(),
+            testableHandler,
+            view
         )
 
         controller.init()
@@ -180,7 +185,7 @@
 
     @Test
     fun testQsExpansionTrue_ignoreAtMostMeasureRequests() {
-        shadeExpansionStateManager.onQsExpansionFractionChanged(0f)
+        qsExpansion.value = 0f
 
         onMeasureListenerCaptor.value.onMeasureAction(
                 getTextLength(shortText).toInt(),
@@ -195,7 +200,7 @@
 
     @Test
     fun testQsExpansionFalse_acceptAtMostMeasureRequests() {
-        shadeExpansionStateManager.onQsExpansionFractionChanged(1f)
+        qsExpansion.value = 1f
 
         onMeasureListenerCaptor.value.onMeasureAction(
                 getTextLength(shortText).toInt(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
index 1bc346d..96db09e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
@@ -57,7 +57,6 @@
             PowerInteractorFactory.create().powerInteractor,
             FakeFeatureFlagsClassic(),
             sceneTestUtils.sceneContainerFlags,
-            sceneTestUtils.deviceEntryRepository,
             FakeKeyguardBouncerRepository(),
             FakeConfigurationRepository(),
             FakeShadeRepository(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/LayoutInflaterUtilTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/LayoutInflaterUtilTest.kt
deleted file mode 100644
index 1c8465a..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/LayoutInflaterUtilTest.kt
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.util.kotlin
-
-import android.content.Context
-import android.testing.AndroidTestingRunner
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.util.view.reinflateAndBindLatest
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.DisposableHandle
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.cancelAndJoin
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.asSharedFlow
-import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.After
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.junit.MockitoJUnit
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-class LayoutInflaterUtilTest : SysuiTestCase() {
-    @JvmField @Rule val mockito = MockitoJUnit.rule()
-
-    private var inflationCount = 0
-    private var callbackCount = 0
-    @Mock private lateinit var disposableHandle: DisposableHandle
-
-    inner class TestLayoutInflater : LayoutInflater(context) {
-        override fun inflate(resource: Int, root: ViewGroup?, attachToRoot: Boolean): View {
-            inflationCount++
-            return View(context)
-        }
-
-        override fun cloneInContext(p0: Context?): LayoutInflater {
-            // not needed for this test
-            return this
-        }
-    }
-
-    val underTest = TestLayoutInflater()
-
-    @After
-    fun cleanUp() {
-        inflationCount = 0
-        callbackCount = 0
-    }
-
-    @Test
-    fun testReinflateAndBindLatest_inflatesWithoutEmission() = runTest {
-        backgroundScope.launch {
-            underTest.reinflateAndBindLatest(
-                resource = 0,
-                root = null,
-                attachToRoot = false,
-                emptyFlow<Unit>()
-            ) {
-                callbackCount++
-                null
-            }
-        }
-
-        // Inflates without an emission
-        runCurrent()
-        assertThat(inflationCount).isEqualTo(1)
-        assertThat(callbackCount).isEqualTo(1)
-    }
-
-    @Test
-    fun testReinflateAndBindLatest_reinflatesOnEmission() = runTest {
-        val observable = MutableSharedFlow<Unit>()
-        val flow = observable.asSharedFlow()
-        backgroundScope.launch {
-            underTest.reinflateAndBindLatest(
-                resource = 0,
-                root = null,
-                attachToRoot = false,
-                flow
-            ) {
-                callbackCount++
-                null
-            }
-        }
-
-        listOf(1, 2, 3).forEach { count ->
-            runCurrent()
-            assertThat(inflationCount).isEqualTo(count)
-            assertThat(callbackCount).isEqualTo(count)
-            observable.emit(Unit)
-        }
-    }
-
-    @Test
-    fun testReinflateAndBindLatest_disposesOnCancel() = runTest {
-        val job = launch {
-            underTest.reinflateAndBindLatest(
-                resource = 0,
-                root = null,
-                attachToRoot = false,
-                emptyFlow()
-            ) {
-                callbackCount++
-                disposableHandle
-            }
-        }
-
-        runCurrent()
-        job.cancelAndJoin()
-        verify(disposableHandle).dispose()
-    }
-}
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 a42fa41..ec808c7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -94,7 +94,6 @@
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository;
-import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.flags.FakeFeatureFlagsClassic;
@@ -403,7 +402,6 @@
                 powerInteractor,
                 featureFlags,
                 sceneContainerFlags,
-                new FakeDeviceEntryRepository(),
                 new FakeKeyguardBouncerRepository(),
                 configurationRepository,
                 shadeRepository,
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 6ef812b..e6e6b7b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
@@ -17,7 +17,6 @@
 
 import static com.android.systemui.Flags.FLAG_EXAMPLE_FLAG;
 
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
@@ -38,11 +37,7 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.uiautomator.UiDevice;
 
-import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.broadcast.FakeBroadcastDispatcher;
-import com.android.systemui.broadcast.logging.BroadcastDispatcherLogger;
-import com.android.systemui.dump.DumpManager;
-import com.android.systemui.settings.UserTracker;
 
 import org.junit.After;
 import org.junit.AfterClass;
@@ -53,7 +48,6 @@
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executor;
 import java.util.concurrent.Future;
 
 /**
@@ -86,7 +80,7 @@
 
     public TestableDependency mDependency;
     private Instrumentation mRealInstrumentation;
-    private FakeBroadcastDispatcher mFakeBroadcastDispatcher;
+    private SysuiTestDependency mSysuiDependency;
 
     @Before
     public void SysuiSetup() throws Exception {
@@ -97,18 +91,8 @@
         // Set the value of a single gantry flag inside the com.android.systemui package to
         // ensure all flags in that package are faked (and thus require a value to be set).
         mSetFlagsRule.disableFlags(FLAG_EXAMPLE_FLAG);
-
-        mDependency = SysuiTestDependencyKt.installSysuiTestDependency(mContext);
-        mFakeBroadcastDispatcher = new FakeBroadcastDispatcher(
-                mContext,
-                mContext.getMainExecutor(),
-                mock(Looper.class),
-                mock(Executor.class),
-                mock(DumpManager.class),
-                mock(BroadcastDispatcherLogger.class),
-                mock(UserTracker.class),
-                shouldFailOnLeakedReceiver());
-
+        mSysuiDependency = new SysuiTestDependency(mContext, shouldFailOnLeakedReceiver());
+        mDependency = mSysuiDependency.install();
         mRealInstrumentation = InstrumentationRegistry.getInstrumentation();
         Instrumentation inst = spy(mRealInstrumentation);
         when(inst.getContext()).thenAnswer(invocation -> {
@@ -120,11 +104,6 @@
                     "SysUI Tests should use SysuiTestCase#getContext or SysuiTestCase#mContext");
         });
         InstrumentationRegistry.registerInstance(inst, InstrumentationRegistry.getArguments());
-        // Many tests end up creating a BroadcastDispatcher. Instead, give them a fake that will
-        // record receivers registered. They are not actually leaked as they are kept just as a weak
-        // reference and are never sent to the Context. This will also prevent a real
-        // BroadcastDispatcher from actually registering receivers.
-        mDependency.injectTestDependency(BroadcastDispatcher.class, mFakeBroadcastDispatcher);
     }
 
     protected boolean shouldFailOnLeakedReceiver() {
@@ -144,8 +123,9 @@
         }
         disallowTestableLooperAsMainThread();
         mContext.cleanUpReceivers(this.getClass().getSimpleName());
-        if (mFakeBroadcastDispatcher != null) {
-            mFakeBroadcastDispatcher.cleanUpReceivers(this.getClass().getSimpleName());
+        FakeBroadcastDispatcher dispatcher = getFakeBroadcastDispatcher();
+        if (dispatcher != null) {
+            dispatcher.cleanUpReceivers(this.getClass().getSimpleName());
         }
     }
 
@@ -172,7 +152,7 @@
     }
 
     public FakeBroadcastDispatcher getFakeBroadcastDispatcher() {
-        return mFakeBroadcastDispatcher;
+        return mSysuiDependency.getFakeBroadcastDispatcher();
     }
 
     public SysuiTestableContext getContext() {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestDependency.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestDependency.kt
index c791f4f..d89d7b0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestDependency.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestDependency.kt
@@ -1,26 +1,60 @@
 package com.android.systemui
 
 import android.annotation.SuppressLint
-import android.content.Context
+import android.os.Looper
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.animation.DialogLaunchAnimator
 import com.android.systemui.animation.fakeDialogLaunchAnimator
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.broadcast.FakeBroadcastDispatcher
+import com.android.systemui.broadcast.logging.BroadcastDispatcherLogger
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.phone.SystemUIDialogManager
+import java.util.concurrent.Executor
+import org.mockito.Mockito.mock
 
-@SuppressLint("VisibleForTests")
-fun installSysuiTestDependency(context: Context): TestableDependency {
-    val initializer: SystemUIInitializer = SystemUIInitializerImpl(context)
-    initializer.init(true)
+class SysuiTestDependency(
+    val context: SysuiTestableContext,
+    private val shouldFailOnLeakedReceiver: Boolean
+) {
+    var fakeBroadcastDispatcher: FakeBroadcastDispatcher? = null
 
-    val dependency = TestableDependency(initializer.sysUIComponent.createDependency())
-    Dependency.setInstance(dependency)
+    @SuppressLint("VisibleForTests")
+    fun install(): TestableDependency {
+        val initializer: SystemUIInitializer = SystemUIInitializerImpl(context)
+        initializer.init(true)
 
-    dependency.injectMockDependency(KeyguardUpdateMonitor::class.java)
+        val dependency = TestableDependency(initializer.sysUIComponent.createDependency())
+        Dependency.setInstance(dependency)
 
-    // Make sure that all tests on any SystemUIDialog does not crash because this dependency
-    // is missing (constructing the actual one would throw).
-    // TODO(b/219008720): Remove this.
-    dependency.injectMockDependency(SystemUIDialogManager::class.java)
-    dependency.injectTestDependency(DialogLaunchAnimator::class.java, fakeDialogLaunchAnimator())
-    return dependency
+        dependency.injectMockDependency(KeyguardUpdateMonitor::class.java)
+
+        // Make sure that all tests on any SystemUIDialog does not crash because this dependency
+        // is missing (constructing the actual one would throw).
+        // TODO(b/219008720): Remove this.
+        dependency.injectMockDependency(SystemUIDialogManager::class.java)
+        dependency.injectTestDependency(
+            DialogLaunchAnimator::class.java,
+            fakeDialogLaunchAnimator()
+        )
+
+        // Many tests end up creating a BroadcastDispatcher. Instead, give them a fake that will
+        // record receivers registered. They are not actually leaked as they are kept just as a weak
+        // reference and are never sent to the Context. This will also prevent a real
+        // BroadcastDispatcher from actually registering receivers.
+        fakeBroadcastDispatcher =
+            FakeBroadcastDispatcher(
+                context,
+                context.mainExecutor,
+                mock(Looper::class.java),
+                mock(Executor::class.java),
+                mock(DumpManager::class.java),
+                mock(BroadcastDispatcherLogger::class.java),
+                mock(UserTracker::class.java),
+                shouldFailOnLeakedReceiver
+            )
+        dependency.injectTestDependency(BroadcastDispatcher::class.java, fakeBroadcastDispatcher)
+        return dependency
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/FakeAccessibilityDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/FakeAccessibilityDataLayerModule.kt
new file mode 100644
index 0000000..baf1006
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/FakeAccessibilityDataLayerModule.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.accessibility.data
+
+import com.android.systemui.accessibility.data.repository.FakeAccessibilityRepositoryModule
+import dagger.Module
+
+@Module(includes = [FakeAccessibilityRepositoryModule::class])
+object FakeAccessibilityDataLayerModule
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityRepository.kt
index 8444c7b..4085b1b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityRepository.kt
@@ -16,8 +16,20 @@
 
 package com.android.systemui.accessibility.data.repository
 
+import com.android.systemui.dagger.SysUISingleton
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
 
+@SysUISingleton
 class FakeAccessibilityRepository(
-    override val isTouchExplorationEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false)
-) : AccessibilityRepository
+    override val isTouchExplorationEnabled: MutableStateFlow<Boolean>,
+) : AccessibilityRepository {
+    @Inject constructor() : this(MutableStateFlow(false))
+}
+
+@Module
+interface FakeAccessibilityRepositoryModule {
+    @Binds fun bindFake(fake: FakeAccessibilityRepository): AccessibilityRepository
+}
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
index cffbf02..36f0882 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/data/FakeSystemUiDataLayerModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/data/FakeSystemUiDataLayerModule.kt
@@ -15,6 +15,7 @@
  */
 package com.android.systemui.data
 
+import com.android.systemui.accessibility.data.FakeAccessibilityDataLayerModule
 import com.android.systemui.authentication.data.FakeAuthenticationDataLayerModule
 import com.android.systemui.bouncer.data.repository.FakeBouncerDataLayerModule
 import com.android.systemui.common.ui.data.FakeCommonDataLayerModule
@@ -30,6 +31,7 @@
 @Module(
     includes =
         [
+            FakeAccessibilityDataLayerModule::class,
             FakeAuthenticationDataLayerModule::class,
             FakeBouncerDataLayerModule::class,
             FakeCommonDataLayerModule::class,
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
index abf72af..6710072 100644
--- 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
@@ -16,6 +16,7 @@
 package com.android.systemui.keyguard.data
 
 import com.android.systemui.keyguard.data.repository.FakeCommandQueueModule
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepositoryModule
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepositoryModule
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepositoryModule
 import dagger.Module
@@ -24,6 +25,7 @@
     includes =
         [
             FakeCommandQueueModule::class,
+            FakeDeviceEntryFaceAuthRepositoryModule::class,
             FakeKeyguardRepositoryModule::class,
             FakeKeyguardTransitionRepositoryModule::class,
         ]
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt
index 322fb28..e289083 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt
@@ -17,15 +17,20 @@
 package com.android.systemui.keyguard.data.repository
 
 import com.android.keyguard.FaceAuthUiEvent
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus
 import com.android.systemui.keyguard.shared.model.FaceDetectionStatus
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.filterNotNull
 
-class FakeDeviceEntryFaceAuthRepository : DeviceEntryFaceAuthRepository {
+@SysUISingleton
+class FakeDeviceEntryFaceAuthRepository @Inject constructor() : DeviceEntryFaceAuthRepository {
 
     override val isAuthenticated = MutableStateFlow(false)
     override val canRunFaceAuth = MutableStateFlow(false)
@@ -66,3 +71,8 @@
         _runningAuthRequest.value = null
     }
 }
+
+@Module
+interface FakeDeviceEntryFaceAuthRepositoryModule {
+    @Binds fun bindFake(fake: FakeDeviceEntryFaceAuthRepository): DeviceEntryFaceAuthRepository
+}
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 fae49b1..88a88c7 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
@@ -58,6 +58,9 @@
     private val _isKeyguardShowing = MutableStateFlow(false)
     override val isKeyguardShowing: Flow<Boolean> = _isKeyguardShowing
 
+    private val _isKeyguardUnlocked = MutableStateFlow(false)
+    override val isKeyguardUnlocked: StateFlow<Boolean> = _isKeyguardUnlocked.asStateFlow()
+
     private val _isKeyguardOccluded = MutableStateFlow(false)
     override val isKeyguardOccluded: Flow<Boolean> = _isKeyguardOccluded
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt
index 82ce802..d2ff9bc5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt
@@ -19,7 +19,6 @@
 
 import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
-import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.repository.FakeCommandQueue
@@ -45,7 +44,6 @@
         sceneContainerFlags: SceneContainerFlags = FakeSceneContainerFlags(),
         repository: FakeKeyguardRepository = FakeKeyguardRepository(),
         commandQueue: FakeCommandQueue = FakeCommandQueue(),
-        deviceEntryRepository: FakeDeviceEntryRepository = FakeDeviceEntryRepository(),
         bouncerRepository: FakeKeyguardBouncerRepository = FakeKeyguardBouncerRepository(),
         configurationRepository: FakeConfigurationRepository = FakeConfigurationRepository(),
         shadeRepository: FakeShadeRepository = FakeShadeRepository(),
@@ -57,7 +55,6 @@
             commandQueue = commandQueue,
             featureFlags = featureFlags,
             sceneContainerFlags = sceneContainerFlags,
-            deviceEntryRepository = deviceEntryRepository,
             bouncerRepository = bouncerRepository,
             configurationRepository = configurationRepository,
             shadeRepository = shadeRepository,
@@ -67,7 +64,6 @@
                 commandQueue = commandQueue,
                 featureFlags = featureFlags,
                 sceneContainerFlags = sceneContainerFlags,
-                deviceEntryRepository = deviceEntryRepository,
                 bouncerRepository = bouncerRepository,
                 configurationRepository = configurationRepository,
                 shadeRepository = shadeRepository,
@@ -87,7 +83,6 @@
         val commandQueue: FakeCommandQueue,
         val featureFlags: FakeFeatureFlags,
         val sceneContainerFlags: SceneContainerFlags,
-        val deviceEntryRepository: FakeDeviceEntryRepository,
         val bouncerRepository: FakeKeyguardBouncerRepository,
         val configurationRepository: FakeConfigurationRepository,
         val shadeRepository: FakeShadeRepository,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeDisabledByPolicyInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeDisabledByPolicyInteractor.kt
index f62bf60..1efa74b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeDisabledByPolicyInteractor.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeDisabledByPolicyInteractor.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.qs.tiles.base.interactor
 
+import android.os.UserHandle
+
 class FakeDisabledByPolicyInteractor : DisabledByPolicyInteractor {
 
     var handleResult: Boolean = false
@@ -23,7 +25,7 @@
         DisabledByPolicyInteractor.PolicyResult.TileEnabled
 
     override suspend fun isDisabled(
-        userId: Int,
+        user: UserHandle,
         userRestriction: String?
     ): DisabledByPolicyInteractor.PolicyResult = policyResult
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileDataInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileDataInteractor.kt
index 5593596..2b3330f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileDataInteractor.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileDataInteractor.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.qs.tiles.base.interactor
 
+import android.os.UserHandle
 import javax.annotation.CheckReturnValue
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
@@ -38,16 +39,16 @@
     fun tryEmitAvailability(isAvailable: Boolean): Boolean = availabilityFlow.tryEmit(isAvailable)
     suspend fun emitAvailability(isAvailable: Boolean) = availabilityFlow.emit(isAvailable)
 
-    override fun tileData(userId: Int, triggers: Flow<DataUpdateTrigger>): Flow<T> {
-        mutableDataRequests.add(DataRequest(userId))
+    override fun tileData(user: UserHandle, triggers: Flow<DataUpdateTrigger>): Flow<T> {
+        mutableDataRequests.add(DataRequest(user))
         return triggers.flatMapLatest { dataFlow }
     }
 
-    override fun availability(userId: Int): Flow<Boolean> {
-        mutableAvailabilityRequests.add(AvailabilityRequest(userId))
+    override fun availability(user: UserHandle): Flow<Boolean> {
+        mutableAvailabilityRequests.add(AvailabilityRequest(user))
         return availabilityFlow
     }
 
-    data class DataRequest(val userId: Int)
-    data class AvailabilityRequest(val userId: Int)
+    data class DataRequest(val user: UserHandle)
+    data class AvailabilityRequest(val user: UserHandle)
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/viewmodel/FakeQSTileConfigProvider.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/viewmodel/FakeQSTileConfigProvider.kt
new file mode 100644
index 0000000..de72a7d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/viewmodel/FakeQSTileConfigProvider.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.tiles.viewmodel
+
+import com.android.systemui.qs.pipeline.shared.TileSpec
+
+class FakeQSTileConfigProvider : QSTileConfigProvider {
+
+    private val configs: MutableMap<String, QSTileConfig> = mutableMapOf()
+
+    override fun getConfig(tileSpec: String): QSTileConfig = configs.getValue(tileSpec)
+
+    fun putConfig(tileSpec: TileSpec, config: QSTileConfig) {
+        configs[tileSpec.spec] = config
+    }
+
+    fun removeConfig(tileSpec: TileSpec): QSTileConfig? = configs.remove(tileSpec.spec)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigTestBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigTestBuilder.kt
index 201926d..2a0ee88 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigTestBuilder.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigTestBuilder.kt
@@ -16,10 +16,7 @@
 
 package com.android.systemui.qs.tiles.viewmodel
 
-import androidx.annotation.StringRes
 import com.android.internal.logging.InstanceId
-import com.android.systemui.common.shared.model.ContentDescription
-import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.qs.pipeline.shared.TileSpec
 
 object QSTileConfigTestBuilder {
@@ -29,8 +26,7 @@
 
     class BuildingScope {
         var tileSpec: TileSpec = TileSpec.create("test_spec")
-        var tileIcon: Icon = Icon.Resource(0, ContentDescription.Resource(0))
-        @StringRes var tileLabel: Int = 0
+        var uiConfig: QSTileUIConfig = QSTileUIConfig.Empty
         var instanceId: InstanceId = InstanceId.fakeInstanceId(0)
         var metricsSpec: String = tileSpec.spec
         var policy: QSTilePolicy = QSTilePolicy.NoRestrictions
@@ -38,8 +34,7 @@
         fun build() =
             QSTileConfig(
                 tileSpec,
-                tileIcon,
-                tileLabel,
+                uiConfig,
                 instanceId,
                 metricsSpec,
                 policy,
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 a4881bc..17384351 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
@@ -185,7 +185,6 @@
             commandQueue = FakeCommandQueue(),
             featureFlags = featureFlags,
             sceneContainerFlags = sceneContainerFlags,
-            deviceEntryRepository = FakeDeviceEntryRepository(),
             bouncerRepository = FakeKeyguardBouncerRepository(),
             configurationRepository = FakeConfigurationRepository(),
             shadeRepository = FakeShadeRepository(),
diff --git a/rs/java/android/renderscript/ScriptC.java b/rs/java/android/renderscript/ScriptC.java
index 1866a99..67c2caa 100644
--- a/rs/java/android/renderscript/ScriptC.java
+++ b/rs/java/android/renderscript/ScriptC.java
@@ -16,9 +16,12 @@
 
 package android.renderscript;
 
+import android.app.compat.CompatChanges;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
 import android.content.res.Resources;
+import android.util.Slog;
 
-import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 
@@ -35,6 +38,15 @@
     private static final String TAG = "ScriptC";
 
     /**
+     * In targetSdkVersion 35 and above, Renderscript's ScriptC stops being supported
+     * and an exception is thrown when the class is instantiated.
+     * In targetSdkVersion 34 and below, Renderscript's ScriptC still works.
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = 35)
+    private static final long RENDERSCRIPT_SCRIPTC_DEPRECATION_CHANGE_ID = 297019750L;
+
+    /**
      * Only intended for use by the generated derived classes.
      *
      * @param id
@@ -89,7 +101,19 @@
         setID(id);
     }
 
+    private static void throwExceptionIfSDKTooHigh() {
+        String message =
+                "ScriptC scripts are not supported when targeting an API Level >= 35. Please refer "
+                    + "to https://developer.android.com/guide/topics/renderscript/migration-guide "
+                    + "for proposed alternatives.";
+        Slog.w(TAG, message);
+        if (CompatChanges.isChangeEnabled(RENDERSCRIPT_SCRIPTC_DEPRECATION_CHANGE_ID)) {
+            throw new UnsupportedOperationException(message);
+        }
+    }
+
     private static synchronized long internalCreate(RenderScript rs, Resources resources, int resourceID) {
+        throwExceptionIfSDKTooHigh();
         byte[] pgm;
         int pgmLength;
         InputStream is = resources.openRawResource(resourceID);
@@ -126,6 +150,7 @@
 
     private static synchronized long internalStringCreate(RenderScript rs, String resName, byte[] bitcode) {
         //        Log.v(TAG, "Create script for resource = " + resName);
+        throwExceptionIfSDKTooHigh();
         return rs.nScriptCCreate(resName, RenderScript.getCachePath(), bitcode, bitcode.length);
     }
 }
diff --git a/services/Android.bp b/services/Android.bp
index 3ae9360..aca8409 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -238,12 +238,14 @@
 stubs_defaults {
     name: "services-stubs-default",
     installable: false,
-    args: " --show-annotation android.annotation.SystemApi\\(client=android.annotation.SystemApi.Client.SYSTEM_SERVER\\)" +
-        " --hide-annotation android.annotation.Hide" +
-        " --hide InternalClasses" + // com.android.* classes are okay in this interface
+    flags: [
+        "--show-annotation android.annotation.SystemApi\\(client=android.annotation.SystemApi.Client.SYSTEM_SERVER\\)",
+        "--hide-annotation android.annotation.Hide",
+        "--hide InternalClasses", // com.android.* classes are okay in this interface
         // TODO: remove the --hide options below
-        " --hide DeprecationMismatch" +
-        " --hide HiddenTypedefConstant",
+        "--hide DeprecationMismatch",
+        "--hide HiddenTypedefConstant",
+    ],
     visibility: ["//frameworks/base:__subpackages__"],
     filter_packages: ["com.android."],
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
index 5af80da..2913716 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
@@ -145,6 +145,13 @@
     /** Flag for intercepting generic motion events. */
     static final int FLAG_FEATURE_INTERCEPT_GENERIC_MOTION_EVENTS = 0x00000800;
 
+    /**
+     * Flag for enabling the two-finger triple-tap magnification feature.
+     *
+     * @see #setUserAndEnabledFeatures(int, int)
+     */
+    static final int FLAG_FEATURE_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP = 0x00001000;
+
     static final int FEATURES_AFFECTING_MOTION_EVENTS =
             FLAG_FEATURE_INJECT_MOTION_EVENTS
                     | FLAG_FEATURE_AUTOCLICK
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 87f9cf1..a7b5325 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -64,7 +64,6 @@
 import android.app.RemoteAction;
 import android.app.admin.DevicePolicyManager;
 import android.appwidget.AppWidgetManagerInternal;
-import android.companion.virtual.VirtualDeviceManager;
 import android.content.ActivityNotFoundException;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -1071,18 +1070,7 @@
         mContext.registerReceiverAsUser(receiver, UserHandle.ALL, filter, null, mMainHandler,
                 Context.RECEIVER_EXPORTED);
 
-        if (android.companion.virtual.flags.Flags.vdmPublicApis()) {
-            VirtualDeviceManager vdm = mContext.getSystemService(VirtualDeviceManager.class);
-            if (vdm != null) {
-                vdm.registerVirtualDeviceListener(mContext.getMainExecutor(),
-                        new VirtualDeviceManager.VirtualDeviceListener() {
-                            @Override
-                            public void onVirtualDeviceClosed(int deviceId) {
-                                mProxyManager.clearConnections(deviceId);
-                            }
-                        });
-            }
-        } else {
+        if (!android.companion.virtual.flags.Flags.vdmPublicApis()) {
             final BroadcastReceiver virtualDeviceReceiver = new BroadcastReceiver() {
                 @Override
                 public void onReceive(Context context, Intent intent) {
@@ -2798,6 +2786,12 @@
                 flags |= AccessibilityInputFilter
                         .FLAG_FEATURE_MAGNIFICATION_SINGLE_FINGER_TRIPLE_TAP;
             }
+            if (Flags.enableMagnificationMultipleFingerMultipleTapGesture()) {
+                if (userState.isMagnificationSingleFingerTripleTapEnabledLocked()) {
+                    flags |= AccessibilityInputFilter
+                            .FLAG_FEATURE_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP;
+                }
+            }
             if (userState.isShortcutMagnificationEnabledLocked()) {
                 flags |= AccessibilityInputFilter.FLAG_FEATURE_TRIGGERED_SCREEN_MAGNIFIER;
             }
@@ -3162,6 +3156,21 @@
         return false;
     }
 
+    private boolean readMagnificationTwoFingerTripleTapSettingsLocked(
+            AccessibilityUserState userState) {
+        final boolean magnificationTwoFingerTripleTapEnabled = Settings.Secure.getIntForUser(
+                mContext.getContentResolver(),
+                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED,
+                0, userState.mUserId) == 1;
+        if ((magnificationTwoFingerTripleTapEnabled
+                != userState.isMagnificationTwoFingerTripleTapEnabledLocked())) {
+            userState.setMagnificationTwoFingerTripleTapEnabledLocked(
+                    magnificationTwoFingerTripleTapEnabled);
+            return true;
+        }
+        return false;
+    }
+
     private boolean readAutoclickEnabledSettingLocked(AccessibilityUserState userState) {
         final boolean autoclickEnabled = Settings.Secure.getIntForUser(
                 mContext.getContentResolver(),
@@ -3397,6 +3406,7 @@
         // displays in one display. It's not a real display and there's no input events for it.
         final ArrayList<Display> displays = getValidDisplayList();
         if (userState.isMagnificationSingleFingerTripleTapEnabledLocked()
+                || userState.isMagnificationTwoFingerTripleTapEnabledLocked()
                 || userState.isShortcutMagnificationEnabledLocked()) {
             for (int i = 0; i < displays.size(); i++) {
                 final Display display = displays.get(i);
@@ -4932,6 +4942,9 @@
         private final Uri mMagnificationmSingleFingerTripleTapEnabledUri = Settings.Secure
                 .getUriFor(Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED);
 
+        private final Uri mMagnificationTwoFingerTripleTapEnabledUri = Settings.Secure.getUriFor(
+                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED);
+
         private final Uri mAutoclickEnabledUri = Settings.Secure.getUriFor(
                 Settings.Secure.ACCESSIBILITY_AUTOCLICK_ENABLED);
 
@@ -4989,6 +5002,10 @@
                     false, this, UserHandle.USER_ALL);
             contentResolver.registerContentObserver(mMagnificationmSingleFingerTripleTapEnabledUri,
                     false, this, UserHandle.USER_ALL);
+            if (Flags.enableMagnificationMultipleFingerMultipleTapGesture()) {
+                contentResolver.registerContentObserver(mMagnificationTwoFingerTripleTapEnabledUri,
+                        false, this, UserHandle.USER_ALL);
+            }
             contentResolver.registerContentObserver(mAutoclickEnabledUri,
                     false, this, UserHandle.USER_ALL);
             contentResolver.registerContentObserver(mEnabledAccessibilityServicesUri,
@@ -5039,6 +5056,11 @@
                     if (readMagnificationEnabledSettingsLocked(userState)) {
                         onUserStateChangedLocked(userState);
                     }
+                } else if (Flags.enableMagnificationMultipleFingerMultipleTapGesture()
+                        && mMagnificationTwoFingerTripleTapEnabledUri.equals(uri)) {
+                    if (readMagnificationTwoFingerTripleTapSettingsLocked(userState)) {
+                        onUserStateChangedLocked(userState);
+                    }
                 } else if (mAutoclickEnabledUri.equals(uri)) {
                     if (readAutoclickEnabledSettingLocked(userState)) {
                         onUserStateChangedLocked(userState);
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
index b4efec1..68ee780 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
@@ -110,6 +110,7 @@
     private boolean mIsAudioDescriptionByDefaultRequested;
     private boolean mIsAutoclickEnabled;
     private boolean mIsMagnificationSingleFingerTripleTapEnabled;
+    private boolean mMagnificationTwoFingerTripleTapEnabled;
     private boolean mIsFilterKeyEventsEnabled;
     private boolean mIsPerformGesturesEnabled;
     private boolean mAccessibilityFocusOnlyInActiveWindow;
@@ -212,6 +213,7 @@
         mRequestTwoFingerPassthrough = false;
         mSendMotionEventsEnabled = false;
         mIsMagnificationSingleFingerTripleTapEnabled = false;
+        mMagnificationTwoFingerTripleTapEnabled = false;
         mIsAutoclickEnabled = false;
         mUserNonInteractiveUiTimeout = 0;
         mUserInteractiveUiTimeout = 0;
@@ -633,6 +635,14 @@
         mIsMagnificationSingleFingerTripleTapEnabled = enabled;
     }
 
+    public boolean isMagnificationTwoFingerTripleTapEnabledLocked() {
+        return mMagnificationTwoFingerTripleTapEnabled;
+    }
+
+    public void setMagnificationTwoFingerTripleTapEnabledLocked(boolean enabled) {
+        mMagnificationTwoFingerTripleTapEnabled = enabled;
+    }
+
     public boolean isFilterKeyEventsEnabledLocked() {
         return mIsFilterKeyEventsEnabled;
     }
diff --git a/services/accessibility/java/com/android/server/accessibility/ProxyManager.java b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java
index ed77476..2032a50 100644
--- a/services/accessibility/java/com/android/server/accessibility/ProxyManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java
@@ -101,6 +101,8 @@
     private VirtualDeviceManagerInternal.AppsOnVirtualDeviceListener
             mAppsOnVirtualDeviceListener;
 
+    private VirtualDeviceManager.VirtualDeviceListener mVirtualDeviceListener;
+
     /**
      * Callbacks into AccessibilityManagerService.
      */
@@ -189,6 +191,9 @@
                     }
                 }
             }
+            if (mProxyA11yServiceConnections.size() == 1) {
+                registerVirtualDeviceListener();
+            }
         }
 
         // If the client dies, make sure to remove the connection.
@@ -210,6 +215,31 @@
         connection.initializeServiceInterface(client);
     }
 
+    private void registerVirtualDeviceListener() {
+        VirtualDeviceManager vdm = mContext.getSystemService(VirtualDeviceManager.class);
+        if (vdm == null || !android.companion.virtual.flags.Flags.vdmPublicApis()) {
+            return;
+        }
+        if (mVirtualDeviceListener == null) {
+            mVirtualDeviceListener = new VirtualDeviceManager.VirtualDeviceListener() {
+                @Override
+                public void onVirtualDeviceClosed(int deviceId) {
+                    clearConnections(deviceId);
+                }
+            };
+        }
+
+        vdm.registerVirtualDeviceListener(mContext.getMainExecutor(), mVirtualDeviceListener);
+    }
+
+    private void unregisterVirtualDeviceListener() {
+        VirtualDeviceManager vdm = mContext.getSystemService(VirtualDeviceManager.class);
+        if (vdm == null || !android.companion.virtual.flags.Flags.vdmPublicApis()) {
+            return;
+        }
+        vdm.unregisterVirtualDeviceListener(mVirtualDeviceListener);
+    }
+
     /**
      * Unregister the proxy based on display id.
      */
@@ -258,6 +288,9 @@
                 deviceId = mProxyA11yServiceConnections.get(displayId).getDeviceId();
                 mProxyA11yServiceConnections.remove(displayId);
                 removedFromConnections = true;
+                if (mProxyA11yServiceConnections.size() == 0) {
+                    unregisterVirtualDeviceListener();
+                }
             }
         }
 
diff --git a/services/autofill/java/com/android/server/autofill/RemoteFillService.java b/services/autofill/java/com/android/server/autofill/RemoteFillService.java
index 423b85f..4688658 100644
--- a/services/autofill/java/com/android/server/autofill/RemoteFillService.java
+++ b/services/autofill/java/com/android/server/autofill/RemoteFillService.java
@@ -56,7 +56,7 @@
     private static final long TIMEOUT_IDLE_BIND_MILLIS = 5 * DateUtils.SECOND_IN_MILLIS;
     private static final long TIMEOUT_REMOTE_REQUEST_MILLIS = 5 * DateUtils.SECOND_IN_MILLIS;
 
-    private FillServiceCallbacks mCallbacks;
+    private final FillServiceCallbacks mCallbacks;
     private final Object mLock = new Object();
     private CompletableFuture<FillResponse> mPendingFillRequest;
     private int mPendingFillRequestId = INVALID_REQUEST_ID;
@@ -128,12 +128,9 @@
      */
     public int cancelCurrentRequest() {
         synchronized (mLock) {
-            int canceledRequestId = mPendingFillRequest != null && mPendingFillRequest.cancel(false)
+            return mPendingFillRequest != null && mPendingFillRequest.cancel(false)
                     ? mPendingFillRequestId
                     : INVALID_REQUEST_ID;
-            mPendingFillRequest = null;
-            mPendingFillRequestId = INVALID_REQUEST_ID;
-            return canceledRequestId;
         }
     }
 
@@ -187,10 +184,6 @@
                 mPendingFillRequest = null;
                 mPendingFillRequestId = INVALID_REQUEST_ID;
             }
-            if (mCallbacks == null) {
-                Slog.w(TAG, "Error calling RemoteFillService - service already unbound");
-                return;
-            }
             if (err == null) {
                 mCallbacks.onFillRequestSuccess(request.getId(), res,
                         mComponentName.getPackageName(), request.getFlags());
@@ -227,10 +220,6 @@
             return save;
         }).orTimeout(TIMEOUT_REMOTE_REQUEST_MILLIS, TimeUnit.MILLISECONDS)
                 .whenComplete((res, err) -> Handler.getMain().post(() -> {
-                    if (mCallbacks == null) {
-                        Slog.w(TAG, "Error calling RemoteFillService - service already unbound");
-                        return;
-                    }
                     if (err == null) {
                         mCallbacks.onSaveRequestSuccess(mComponentName.getPackageName(), res);
                     } else {
@@ -245,8 +234,6 @@
     }
 
     public void destroy() {
-        cancelCurrentRequest();
         unbind();
-        mCallbacks = null;
     }
 }
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 961e9d3..3985a0e 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -155,6 +155,7 @@
         "service-permission.stubs.system_server",
         "service-rkp.stubs.system_server",
         "service-sdksandbox.stubs.system_server",
+        "device_policy_aconfig_flags_lib",
     ],
     plugins: ["ImmutabilityAnnotationProcessor"],
 
diff --git a/services/core/java/com/android/server/BrickReceiver.java b/services/core/java/com/android/server/BrickReceiver.java
deleted file mode 100644
index cff3805..0000000
--- a/services/core/java/com/android/server/BrickReceiver.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2008 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;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.BroadcastReceiver;
-import android.os.SystemService;
-import android.util.Slog;
-
-public class BrickReceiver extends BroadcastReceiver {
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        Slog.w("BrickReceiver", "!!! BRICKING DEVICE !!!");
-        SystemService.start("brick");
-    }
-}
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 3af0e8c..15fc2dc 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -3007,7 +3007,7 @@
             // We need all the users unlocked to move their primary storage
             users = mContext.getSystemService(UserManager.class).getUsers();
             for (UserInfo user : users) {
-                if (StorageManager.isFileEncrypted() && !isUserKeyUnlocked(user.id)) {
+                if (StorageManager.isFileEncrypted() && !isCeStorageUnlocked(user.id)) {
                     Slog.w(TAG, "Failing move due to locked user " + user.id);
                     onMoveStatusLocked(PackageManager.MOVE_FAILED_LOCKED_USER);
                     return;
@@ -3231,12 +3231,12 @@
 
     @android.annotation.EnforcePermission(android.Manifest.permission.STORAGE_INTERNAL)
     @Override
-    public void createUserKey(int userId, int serialNumber, boolean ephemeral) {
+    public void createUserStorageKeys(int userId, int serialNumber, boolean ephemeral) {
 
-        super.createUserKey_enforcePermission();
+        super.createUserStorageKeys_enforcePermission();
 
         try {
-            mVold.createUserKey(userId, serialNumber, ephemeral);
+            mVold.createUserStorageKeys(userId, serialNumber, ephemeral);
             // Since the user's CE key was just created, the user's CE storage is now unlocked.
             synchronized (mLock) {
                 mCeUnlockedUsers.append(userId);
@@ -3248,12 +3248,12 @@
 
     @android.annotation.EnforcePermission(android.Manifest.permission.STORAGE_INTERNAL)
     @Override
-    public void destroyUserKey(int userId) {
+    public void destroyUserStorageKeys(int userId) {
 
-        super.destroyUserKey_enforcePermission();
+        super.destroyUserStorageKeys_enforcePermission();
 
         try {
-            mVold.destroyUserKey(userId);
+            mVold.destroyUserStorageKeys(userId);
             // Since the user's CE key was just destroyed, the user's CE storage is now locked.
             synchronized (mLock) {
                 mCeUnlockedUsers.remove(userId);
@@ -3266,21 +3266,22 @@
     /* Only for use by LockSettingsService */
     @android.annotation.EnforcePermission(android.Manifest.permission.STORAGE_INTERNAL)
     @Override
-    public void setUserKeyProtection(@UserIdInt int userId, byte[] secret) throws RemoteException {
-        super.setUserKeyProtection_enforcePermission();
+    public void setCeStorageProtection(@UserIdInt int userId, byte[] secret)
+            throws RemoteException {
+        super.setCeStorageProtection_enforcePermission();
 
-        mVold.setUserKeyProtection(userId, HexDump.toHexString(secret));
+        mVold.setCeStorageProtection(userId, HexDump.toHexString(secret));
     }
 
     /* Only for use by LockSettingsService */
     @android.annotation.EnforcePermission(android.Manifest.permission.STORAGE_INTERNAL)
     @Override
-    public void unlockUserKey(@UserIdInt int userId, int serialNumber, byte[] secret)
-        throws RemoteException {
-        super.unlockUserKey_enforcePermission();
+    public void unlockCeStorage(@UserIdInt int userId, int serialNumber, byte[] secret)
+            throws RemoteException {
+        super.unlockCeStorage_enforcePermission();
 
         if (StorageManager.isFileEncrypted()) {
-            mVold.unlockUserKey(userId, serialNumber, HexDump.toHexString(secret));
+            mVold.unlockCeStorage(userId, serialNumber, HexDump.toHexString(secret));
         }
         synchronized (mLock) {
             mCeUnlockedUsers.append(userId);
@@ -3289,23 +3290,22 @@
 
     @android.annotation.EnforcePermission(android.Manifest.permission.STORAGE_INTERNAL)
     @Override
-    public void lockUserKey(int userId) {
-        //  Do not lock user 0 data for headless system user
-        super.lockUserKey_enforcePermission();
+    public void lockCeStorage(int userId) {
+        super.lockCeStorage_enforcePermission();
 
+        // Never lock the CE storage of a headless system user.
         if (userId == UserHandle.USER_SYSTEM
                 && UserManager.isHeadlessSystemUserMode()) {
             throw new IllegalArgumentException("Headless system user data cannot be locked..");
         }
 
-
-        if (!isUserKeyUnlocked(userId)) {
+        if (!isCeStorageUnlocked(userId)) {
             Slog.d(TAG, "User " + userId + "'s CE storage is already locked");
             return;
         }
 
         try {
-            mVold.lockUserKey(userId);
+            mVold.lockCeStorage(userId);
         } catch (Exception e) {
             Slog.wtf(TAG, e);
             return;
@@ -3317,7 +3317,7 @@
     }
 
     @Override
-    public boolean isUserKeyUnlocked(int userId) {
+    public boolean isCeStorageUnlocked(int userId) {
         synchronized (mLock) {
             return mCeUnlockedUsers.contains(userId);
         }
@@ -3719,8 +3719,8 @@
         final int userId = UserHandle.getUserId(callingUid);
         final String propertyName = "sys.user." + userId + ".ce_available";
 
-        // Ignore requests to create directories while storage is locked
-        if (!isUserKeyUnlocked(userId)) {
+        // Ignore requests to create directories while CE storage is locked
+        if (!isCeStorageUnlocked(userId)) {
             throw new IllegalStateException("Failed to prepare " + appPath);
         }
 
@@ -3846,15 +3846,15 @@
         final boolean systemUserUnlocked = isSystemUnlocked(UserHandle.USER_SYSTEM);
 
         final boolean userIsDemo;
-        final boolean userKeyUnlocked;
         final boolean storagePermission;
+        final boolean ceStorageUnlocked;
         final long token = Binder.clearCallingIdentity();
         try {
             userIsDemo = LocalServices.getService(UserManagerInternal.class)
                     .getUserInfo(userId).isDemo();
             storagePermission = mStorageManagerInternal.hasExternalStorage(callingUid,
                     callingPackage);
-            userKeyUnlocked = isUserKeyUnlocked(userId);
+            ceStorageUnlocked = isCeStorageUnlocked(userId);
         } finally {
             Binder.restoreCallingIdentity(token);
         }
@@ -3914,7 +3914,7 @@
                 } else if (!systemUserUnlocked) {
                     reportUnmounted = true;
                     Slog.w(TAG, "Reporting " + volId + " unmounted due to system locked");
-                } else if ((vol.getType() == VolumeInfo.TYPE_EMULATED) && !userKeyUnlocked) {
+                } else if ((vol.getType() == VolumeInfo.TYPE_EMULATED) && !ceStorageUnlocked) {
                     reportUnmounted = true;
                     Slog.w(TAG, "Reporting " + volId + "unmounted due to " + userId + " locked");
                 } else if (!storagePermission && !realState) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index ef67cbe..e88d0c6 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -2299,7 +2299,7 @@
             return;
         }
         // TODO(b/148767783): should we check all profiles under user0?
-        UserspaceRebootLogger.logEventAsync(StorageManager.isUserKeyUnlocked(userId),
+        UserspaceRebootLogger.logEventAsync(StorageManager.isCeStorageUnlocked(userId),
                 BackgroundThread.getExecutor());
     }
 
@@ -4648,7 +4648,7 @@
             // We carefully use the same state that PackageManager uses for
             // filtering, since we use this flag to decide if we need to install
             // providers when user is unlocked later
-            app.setUnlocked(StorageManager.isUserKeyUnlocked(app.userId));
+            app.setUnlocked(StorageManager.isCeStorageUnlocked(app.userId));
         }
 
         boolean normalMode = mProcessesReady || isAllowedWhileBooting(app.info);
@@ -9718,9 +9718,29 @@
     public ParceledListSlice<ApplicationStartInfo> getHistoricalProcessStartReasons(
             String packageName, int maxNum, int userId) {
         enforceNotIsolatedCaller("getHistoricalProcessStartReasons");
+        // For the simplification, we don't support USER_ALL nor USER_CURRENT here.
+        if (userId == UserHandle.USER_ALL || userId == UserHandle.USER_CURRENT) {
+            throw new IllegalArgumentException("Unsupported userId");
+        }
+
+        final int callingPid = Binder.getCallingPid();
+        final int callingUid = Binder.getCallingUid();
+        mUserController.handleIncomingUser(callingPid, callingUid, userId, true, ALLOW_NON_FULL,
+                "getHistoricalProcessStartReasons", null);
 
         final ArrayList<ApplicationStartInfo> results = new ArrayList<ApplicationStartInfo>();
-
+        if (!TextUtils.isEmpty(packageName)) {
+            final int uid = enforceDumpPermissionForPackage(packageName, userId, callingUid,
+                        "getHistoricalProcessStartReasons");
+            if (uid != INVALID_UID) {
+                mProcessList.mAppStartInfoTracker.getStartInfo(
+                        packageName, userId, callingPid, maxNum, results);
+            }
+        } else {
+            // If no package name is given, use the caller's uid as the filter uid.
+            mProcessList.mAppStartInfoTracker.getStartInfo(
+                    packageName, callingUid, callingPid, maxNum, results);
+        }
         return new ParceledListSlice<ApplicationStartInfo>(results);
     }
 
@@ -9729,6 +9749,14 @@
     public void setApplicationStartInfoCompleteListener(
             IApplicationStartInfoCompleteListener listener, int userId) {
         enforceNotIsolatedCaller("setApplicationStartInfoCompleteListener");
+
+        // For the simplification, we don't support USER_ALL nor USER_CURRENT here.
+        if (userId == UserHandle.USER_ALL || userId == UserHandle.USER_CURRENT) {
+            throw new IllegalArgumentException("Unsupported userId");
+        }
+
+        final int callingUid = Binder.getCallingUid();
+        mProcessList.mAppStartInfoTracker.addStartInfoCompleteListener(listener, callingUid);
     }
 
 
@@ -9742,6 +9770,7 @@
         }
 
         final int callingUid = Binder.getCallingUid();
+        mProcessList.mAppStartInfoTracker.clearStartInfoCompleteListener(callingUid, true);
     }
 
     @Override
@@ -10043,6 +10072,8 @@
             pw.println();
             if (dumpAll) {
                 pw.println("-------------------------------------------------------------------------------");
+                mProcessList.mAppStartInfoTracker.dumpHistoryProcessStartInfo(pw, dumpPackage);
+                pw.println("-------------------------------------------------------------------------------");
                 mProcessList.mAppExitInfoTracker.dumpHistoryProcessExitInfo(pw, dumpPackage);
             }
             if (dumpPackage == null) {
@@ -10439,6 +10470,12 @@
                 LockGuard.dump(fd, pw, args);
             } else if ("users".equals(cmd)) {
                 dumpUsers(pw);
+            } else if ("start-info".equals(cmd)) {
+                if (opti < args.length) {
+                    dumpPackage = args[opti];
+                    opti++;
+                }
+                mProcessList.mAppStartInfoTracker.dumpHistoryProcessStartInfo(pw, dumpPackage);
             } else if ("exit-info".equals(cmd)) {
                 if (opti < args.length) {
                     dumpPackage = args[opti];
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index a057f32..69bf612 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -272,6 +272,8 @@
                     return runSetWatchHeap(pw);
                 case "clear-watch-heap":
                     return runClearWatchHeap(pw);
+                case "clear-start-info":
+                    return runClearStartInfo(pw);
                 case "clear-exit-info":
                     return runClearExitInfo(pw);
                 case "bug-report":
@@ -1339,6 +1341,31 @@
         return 0;
     }
 
+    int runClearStartInfo(PrintWriter pw) throws RemoteException {
+        mInternal.enforceCallingPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS,
+                "runClearStartInfo()");
+        String opt;
+        int userId = UserHandle.USER_CURRENT;
+        String packageName = null;
+        while ((opt = getNextOption()) != null) {
+            if (opt.equals("--user")) {
+                userId = UserHandle.parseUserArg(getNextArgRequired());
+            } else {
+                packageName = opt;
+            }
+        }
+        if (userId == UserHandle.USER_CURRENT) {
+            UserInfo user = mInterface.getCurrentUser();
+            if (user == null) {
+                return -1;
+            }
+            userId = user.id;
+        }
+        mInternal.mProcessList.mAppStartInfoTracker
+                .clearHistoryProcessStartInfo(packageName, userId);
+        return 0;
+    }
+
     int runClearExitInfo(PrintWriter pw) throws RemoteException {
         mInternal.enforceCallingPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS,
                 "runClearExitInfo()");
@@ -4090,6 +4117,7 @@
             pw.println("    s[ervices] [COMP_SPEC ...]: service state");
             pw.println("    allowed-associations: current package association restrictions");
             pw.println("    as[sociations]: tracked app associations");
+            pw.println("    start-info [PACKAGE_NAME]: historical process start information");
             pw.println("    exit-info [PACKAGE_NAME]: historical process exit information");
             pw.println("    lmk: stats on low memory killer");
             pw.println("    lru: raw LRU process list");
@@ -4265,6 +4293,8 @@
             pw.println("      above <HEAP-LIMIT> then a heap dump is collected for the user to report.");
             pw.println("  clear-watch-heap");
             pw.println("      Clear the previously set-watch-heap.");
+            pw.println("  clear-start-info [--user <USER_ID> | all | current] [package]");
+            pw.println("      Clear the process start-info for given package");
             pw.println("  clear-exit-info [--user <USER_ID> | all | current] [package]");
             pw.println("      Clear the process exit-info for given package");
             pw.println("  bug-report [--progress | --telephony]");
diff --git a/services/core/java/com/android/server/am/AppStartInfoTracker.java b/services/core/java/com/android/server/am/AppStartInfoTracker.java
new file mode 100644
index 0000000..edca74f
--- /dev/null
+++ b/services/core/java/com/android/server/am/AppStartInfoTracker.java
@@ -0,0 +1,989 @@
+/*
+ * 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.server.am;
+
+import static android.app.ApplicationStartInfo.START_TIMESTAMP_LAUNCH;
+import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
+
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
+
+import android.app.ActivityOptions;
+import android.app.ApplicationStartInfo;
+import android.app.Flags;
+import android.app.IApplicationStartInfoCompleteListener;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.icu.text.SimpleDateFormat;
+import android.os.Binder;
+import android.os.FileUtils;
+import android.os.Handler;
+import android.os.IBinder.DeathRecipient;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.AtomicFile;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.proto.ProtoInputStream;
+import android.util.proto.ProtoOutputStream;
+import android.util.proto.WireTypeMismatchException;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.ProcessMap;
+import com.android.server.IoThread;
+import com.android.server.ServiceThread;
+import com.android.server.SystemServiceManager;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.BiFunction;
+
+/** A class to manage all the {@link android.app.ApplicationStartInfo} records. */
+public final class AppStartInfoTracker {
+    private static final String TAG = TAG_WITH_CLASS_NAME ? "AppStartInfoTracker" : TAG_AM;
+
+    /** Interval of persisting the app start info to persistent storage. */
+    private static final long APP_START_INFO_PERSIST_INTERVAL = TimeUnit.MINUTES.toMillis(30);
+
+    /** These are actions that the forEach* should take after each iteration */
+    private static final int FOREACH_ACTION_NONE = 0;
+    private static final int FOREACH_ACTION_REMOVE_ITEM = 1;
+    private static final int FOREACH_ACTION_STOP_ITERATION = 2;
+
+    private static final int APP_START_INFO_HISTORY_LIST_SIZE = 16;
+
+    @VisibleForTesting static final String APP_START_STORE_DIR = "procstartstore";
+
+    @VisibleForTesting static final String APP_START_INFO_FILE = "procstartinfo";
+
+    private final Object mLock = new Object();
+
+    private boolean mEnabled = false;
+
+    /** Initialized in {@link #init} and read-only after that. */
+    private ActivityManagerService mService;
+
+    /** Initialized in {@link #init} and read-only after that. */
+    private Handler mHandler;
+
+    /** The task to persist app process start info */
+    @GuardedBy("mLock")
+    private Runnable mAppStartInfoPersistTask = null;
+
+    /**
+     * Last time(in ms) since epoch that the app start info was persisted into persistent storage.
+     */
+    @GuardedBy("mLock")
+    private long mLastAppStartInfoPersistTimestamp = 0L;
+
+    /**
+     * Retention policy: keep up to X historical start info per package.
+     *
+     * <p>Initialized in {@link #init} and read-only after that. No lock is needed.
+     */
+    private int mAppStartInfoHistoryListSize;
+
+    @GuardedBy("mLock")
+    private final ProcessMap<AppStartInfoContainer> mData;
+
+    /** UID as key. */
+    @GuardedBy("mLock")
+    private final SparseArray<ApplicationStartInfoCompleteCallback> mCallbacks;
+
+    /**
+     * Whether or not we've loaded the historical app process start info from persistent storage.
+     */
+    @VisibleForTesting AtomicBoolean mAppStartInfoLoaded = new AtomicBoolean();
+
+    /** Temporary list being used to filter/sort intermediate results in {@link #getStartInfo}. */
+    @GuardedBy("mLock")
+    final ArrayList<ApplicationStartInfo> mTmpStartInfoList = new ArrayList<>();
+
+    /**
+     * The path to the directory which includes the historical proc start info file as specified in
+     * {@link #mProcStartInfoFile}.
+     */
+    @VisibleForTesting File mProcStartStoreDir;
+
+    /** The path to the historical proc start info file, persisted in the storage. */
+    @VisibleForTesting File mProcStartInfoFile;
+
+    AppStartInfoTracker() {
+        mCallbacks = new SparseArray<>();
+        mData = new ProcessMap<AppStartInfoContainer>();
+    }
+
+    void init(ActivityManagerService service) {
+        mService = service;
+
+        ServiceThread thread =
+                new ServiceThread(TAG + ":handler", THREAD_PRIORITY_BACKGROUND, true /* allowIo */);
+        thread.start();
+        mHandler = new Handler(thread.getLooper());
+
+        mProcStartStoreDir = new File(SystemServiceManager.ensureSystemDir(), APP_START_STORE_DIR);
+        if (!FileUtils.createDir(mProcStartStoreDir)) {
+            Slog.e(TAG, "Unable to create " + mProcStartStoreDir);
+            return;
+        }
+        mProcStartInfoFile = new File(mProcStartStoreDir, APP_START_INFO_FILE);
+
+        mAppStartInfoHistoryListSize = APP_START_INFO_HISTORY_LIST_SIZE;
+    }
+
+    void onSystemReady() {
+        mEnabled = Flags.appStartInfo();
+        if (!mEnabled) {
+            return;
+        }
+
+        registerForUserRemoval();
+        registerForPackageRemoval();
+        IoThread.getHandler().post(() -> {
+            loadExistingProcessStartInfo();
+        });
+    }
+
+    void handleProcessColdStarted(long startTimeNs, HostingRecord hostingRecord,
+            ProcessRecord app) {
+        synchronized (mLock) {
+            if (!mEnabled) {
+                return;
+            }
+            ApplicationStartInfo start = new ApplicationStartInfo();
+            addBaseFieldsFromProcessRecord(start, app);
+            start.setStartupState(ApplicationStartInfo.STARTUP_STATE_STARTED);
+            start.addStartupTimestamp(
+                    ApplicationStartInfo.START_TIMESTAMP_LAUNCH, startTimeNs);
+            start.addStartupTimestamp(
+                    ApplicationStartInfo.START_TIMESTAMP_FORK, app.getStartElapsedTime());
+            start.setStartType(ApplicationStartInfo.START_TYPE_COLD);
+            start.setReason(ApplicationStartInfo.START_REASON_OTHER);
+            addStartInfoLocked(start);
+        }
+    }
+
+    public void handleProcessActivityWarmOrHotStarted(long startTimeNs,
+            ActivityOptions activityOptions, Intent intent) {
+        synchronized (mLock) {
+            if (!mEnabled) {
+                return;
+            }
+            ApplicationStartInfo start = new ApplicationStartInfo();
+            start.setStartupState(ApplicationStartInfo.STARTUP_STATE_STARTED);
+            start.addStartupTimestamp(
+                    ApplicationStartInfo.START_TIMESTAMP_LAUNCH, startTimeNs);
+            start.setIntent(intent);
+            start.setReason(ApplicationStartInfo.START_REASON_LAUNCHER);
+            if (activityOptions != null) {
+                start.setProcessName(activityOptions.getPackageName());
+            }
+            start.setStartType(ApplicationStartInfo.START_TYPE_WARM);
+            if (intent != null && intent.getCategories() != null
+                    && intent.getCategories().contains(Intent.CATEGORY_LAUNCHER)) {
+                start.setReason(ApplicationStartInfo.START_REASON_LAUNCHER);
+            } else {
+                start.setReason(ApplicationStartInfo.START_REASON_START_ACTIVITY);
+            }
+            addStartInfoLocked(start);
+        }
+    }
+
+    public void handleProcessActivityStartedFromRecents(long startTimeNs,
+            ActivityOptions activityOptions) {
+        synchronized (mLock) {
+            if (!mEnabled) {
+                return;
+            }
+            ApplicationStartInfo start = new ApplicationStartInfo();
+            start.setStartupState(ApplicationStartInfo.STARTUP_STATE_STARTED);
+            start.addStartupTimestamp(
+                    ApplicationStartInfo.START_TIMESTAMP_LAUNCH, startTimeNs);
+            if (activityOptions != null) {
+                start.setIntent(activityOptions.getResultData());
+                start.setProcessName(activityOptions.getPackageName());
+            }
+            start.setReason(ApplicationStartInfo.START_REASON_LAUNCHER_RECENTS);
+            start.setStartType(ApplicationStartInfo.START_TYPE_WARM);
+            addStartInfoLocked(start);
+        }
+    }
+
+    public void handleProcessServiceStart(long startTimeNs, ProcessRecord app,
+                ServiceRecord serviceRecord, HostingRecord hostingRecord, boolean cold) {
+        synchronized (mLock) {
+            if (!mEnabled) {
+                return;
+            }
+            ApplicationStartInfo start = new ApplicationStartInfo();
+            addBaseFieldsFromProcessRecord(start, app);
+            start.setStartupState(ApplicationStartInfo.STARTUP_STATE_STARTED);
+            start.addStartupTimestamp(
+                    ApplicationStartInfo.START_TIMESTAMP_LAUNCH, startTimeNs);
+            start.setStartType(cold ? ApplicationStartInfo.START_TYPE_COLD
+                    : ApplicationStartInfo.START_TYPE_WARM);
+            start.setReason(serviceRecord.permission != null
+                    && serviceRecord.permission.contains("android.permission.BIND_JOB_SERVICE")
+                    ? ApplicationStartInfo.START_REASON_JOB
+                    : ApplicationStartInfo.START_REASON_SERVICE);
+            start.setIntent(serviceRecord.intent.getIntent());
+            addStartInfoLocked(start);
+        }
+    }
+
+    public void handleProcessBroadcastStart(long startTimeNs, ProcessRecord app,
+                BroadcastRecord broadcast, boolean cold) {
+        synchronized (mLock) {
+            if (!mEnabled) {
+                return;
+            }
+            ApplicationStartInfo start = new ApplicationStartInfo();
+            addBaseFieldsFromProcessRecord(start, app);
+            start.setStartupState(ApplicationStartInfo.STARTUP_STATE_STARTED);
+            start.addStartupTimestamp(
+                    ApplicationStartInfo.START_TIMESTAMP_LAUNCH, startTimeNs);
+            start.setStartType(cold ? ApplicationStartInfo.START_TYPE_COLD
+                    : ApplicationStartInfo.START_TYPE_WARM);
+            if (broadcast == null) {
+                start.setReason(ApplicationStartInfo.START_REASON_BROADCAST);
+            } else if (broadcast.alarm) {
+                start.setReason(ApplicationStartInfo.START_REASON_ALARM);
+            } else if (broadcast.pushMessage || broadcast.pushMessageOverQuota) {
+                start.setReason(ApplicationStartInfo.START_REASON_PUSH);
+            } else if (Intent.ACTION_BOOT_COMPLETED.equals(broadcast.intent.getAction())) {
+                start.setReason(ApplicationStartInfo.START_REASON_BOOT_COMPLETE);
+            } else {
+                start.setReason(ApplicationStartInfo.START_REASON_BROADCAST);
+            }
+            start.setIntent(broadcast != null ? broadcast.intent : null);
+            addStartInfoLocked(start);
+        }
+    }
+
+    public void handleProcessContentProviderStart(long startTimeNs, ProcessRecord app,
+                boolean cold) {
+        synchronized (mLock) {
+            if (!mEnabled) {
+                return;
+            }
+            ApplicationStartInfo start = new ApplicationStartInfo();
+            addBaseFieldsFromProcessRecord(start, app);
+            start.setStartupState(ApplicationStartInfo.STARTUP_STATE_STARTED);
+            start.addStartupTimestamp(
+                    ApplicationStartInfo.START_TIMESTAMP_LAUNCH, startTimeNs);
+            start.setStartType(cold ? ApplicationStartInfo.START_TYPE_COLD
+                    : ApplicationStartInfo.START_TYPE_WARM);
+            start.setReason(ApplicationStartInfo.START_REASON_CONTENT_PROVIDER);
+            addStartInfoLocked(start);
+        }
+    }
+
+    public void handleProcessBackupStart(long startTimeNs, ProcessRecord app,
+                BackupRecord backupRecord, boolean cold) {
+        synchronized (mLock) {
+            if (!mEnabled) {
+                return;
+            }
+            ApplicationStartInfo start = new ApplicationStartInfo();
+            addBaseFieldsFromProcessRecord(start, app);
+            start.setStartupState(ApplicationStartInfo.STARTUP_STATE_STARTED);
+            start.addStartupTimestamp(
+                ApplicationStartInfo.START_TIMESTAMP_LAUNCH, startTimeNs);
+            start.setStartType(cold ? ApplicationStartInfo.START_TYPE_COLD
+                    : ApplicationStartInfo.START_TYPE_WARM);
+            start.setReason(ApplicationStartInfo.START_REASON_BACKUP);
+            addStartInfoLocked(start);
+        }
+    }
+
+    private void addBaseFieldsFromProcessRecord(ApplicationStartInfo start, ProcessRecord app) {
+        if (app == null) {
+            return;
+        }
+        final int definingUid = app.getHostingRecord() != null
+                ? app.getHostingRecord().getDefiningUid() : 0;
+        start.setPid(app.getPid());
+        start.setRealUid(app.uid);
+        start.setPackageUid(app.info.uid);
+        start.setDefiningUid(definingUid > 0 ? definingUid : app.info.uid);
+        start.setProcessName(app.processName);
+    }
+
+    void reportApplicationOnCreateTimeNanos(ProcessRecord app, long timeNs) {
+        if (!mEnabled) {
+            return;
+        }
+        addTimestampToStart(app, timeNs,
+                ApplicationStartInfo.START_TIMESTAMP_APPLICATION_ONCREATE);
+    }
+
+    void reportBindApplicationTimeNanos(ProcessRecord app, long timeNs) {
+        addTimestampToStart(app, timeNs,
+                ApplicationStartInfo.START_TIMESTAMP_BIND_APPLICATION);
+    }
+
+    void reportFirstFrameTimeNanos(ProcessRecord app, long timeNs) {
+        if (!mEnabled) {
+            return;
+        }
+        addTimestampToStart(app, timeNs,
+                ApplicationStartInfo.START_TIMESTAMP_FIRST_FRAME);
+    }
+
+    void reportFullyDrawnTimeNanos(ProcessRecord app, long timeNs) {
+        if (!mEnabled) {
+            return;
+        }
+        addTimestampToStart(app, timeNs,
+                ApplicationStartInfo.START_TIMESTAMP_FULLY_DRAWN);
+    }
+
+    void reportFullyDrawnTimeNanos(String processName, int uid, long timeNs) {
+        if (!mEnabled) {
+            return;
+        }
+        addTimestampToStart(processName, uid, timeNs,
+                ApplicationStartInfo.START_TIMESTAMP_FULLY_DRAWN);
+    }
+
+    private void addTimestampToStart(ProcessRecord app, long timeNs, int key) {
+        addTimestampToStart(app.processName, app.uid, timeNs, key);
+    }
+
+    private void addTimestampToStart(String processName, int uid, long timeNs, int key) {
+        synchronized (mLock) {
+            AppStartInfoContainer container = mData.get(processName, uid);
+            if (container == null) {
+                // Record was not created, discard new data.
+                return;
+            }
+            container.addTimestampToStartLocked(key, timeNs);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private ApplicationStartInfo addStartInfoLocked(ApplicationStartInfo raw) {
+        if (!mAppStartInfoLoaded.get()) {
+            //records added before initial load from storage will be lost.
+            Slog.w(TAG, "Skipping saving the start info due to ongoing loading from storage");
+            return null;
+        }
+
+        final ApplicationStartInfo info = new ApplicationStartInfo(raw);
+
+        AppStartInfoContainer container = mData.get(raw.getProcessName(), raw.getRealUid());
+        if (container == null) {
+            container = new AppStartInfoContainer(mAppStartInfoHistoryListSize);
+            container.mUid = info.getRealUid();
+            mData.put(raw.getProcessName(), raw.getRealUid(), container);
+        }
+        container.addStartInfoLocked(info);
+
+        schedulePersistProcessStartInfo(false);
+
+        return info;
+    }
+
+    /**
+     * Called whenever data is added to a {@link ApplicationStartInfo} object. Checks for
+     * completeness and triggers callback if a callback has been registered and the object
+     * is complete.
+     */
+    private void checkCompletenessAndCallback(ApplicationStartInfo startInfo) {
+        synchronized (mLock) {
+            if (startInfo.getStartupState()
+                    == ApplicationStartInfo.STARTUP_STATE_FIRST_FRAME_DRAWN) {
+                ApplicationStartInfoCompleteCallback callback =
+                        mCallbacks.get(startInfo.getRealUid());
+                if (callback != null) {
+                    callback.onApplicationStartInfoComplete(startInfo);
+                }
+            }
+        }
+    }
+
+    void getStartInfo(String packageName, int filterUid, int filterPid,
+            int maxNum, ArrayList<ApplicationStartInfo> results) {
+        if (!mEnabled) {
+            return;
+        }
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                boolean emptyPackageName = TextUtils.isEmpty(packageName);
+                if (!emptyPackageName) {
+                    // fast path
+                    AppStartInfoContainer container = mData.get(packageName, filterUid);
+                    if (container != null) {
+                        container.getStartInfoLocked(filterPid, maxNum, results);
+                    }
+                } else {
+                    // slow path
+                    final ArrayList<ApplicationStartInfo> list = mTmpStartInfoList;
+                    list.clear();
+                    // get all packages
+                    forEachPackageLocked(
+                            (name, records) -> {
+                                AppStartInfoContainer container = records.get(filterUid);
+                                if (container != null) {
+                                    list.addAll(container.mInfos);
+                                }
+                                return AppStartInfoTracker.FOREACH_ACTION_NONE;
+                            });
+
+                    Collections.sort(
+                            list, (a, b) ->
+                            Long.compare(getStartTimestamp(b), getStartTimestamp(a)));
+                    int size = list.size();
+                    if (maxNum > 0) {
+                        size = Math.min(size, maxNum);
+                    }
+                    for (int i = 0; i < size; i++) {
+                        results.add(list.get(i));
+                    }
+                    list.clear();
+                }
+            }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    final class ApplicationStartInfoCompleteCallback implements DeathRecipient {
+        private final int mUid;
+        private final IApplicationStartInfoCompleteListener mCallback;
+
+        ApplicationStartInfoCompleteCallback(IApplicationStartInfoCompleteListener callback,
+                int uid) {
+            mCallback = callback;
+            mUid = uid;
+            try {
+                mCallback.asBinder().linkToDeath(this, 0);
+            } catch (RemoteException e) {
+                /*ignored*/
+            }
+        }
+
+        void onApplicationStartInfoComplete(ApplicationStartInfo startInfo) {
+            try {
+                mCallback.onApplicationStartInfoComplete(startInfo);
+            } catch (RemoteException e) {
+                /*ignored*/
+            }
+            clearStartInfoCompleteListener(mUid, true);
+        }
+
+        void unlinkToDeath() {
+            mCallback.asBinder().unlinkToDeath(this, 0);
+        }
+
+        @Override
+        public void binderDied() {
+            clearStartInfoCompleteListener(mUid, false);
+        }
+    }
+
+    void addStartInfoCompleteListener(
+            final IApplicationStartInfoCompleteListener listener, final int uid) {
+        synchronized (mLock) {
+            if (!mEnabled) {
+                return;
+            }
+            mCallbacks.put(uid, new ApplicationStartInfoCompleteCallback(listener, uid));
+        }
+    }
+
+    void clearStartInfoCompleteListener(final int uid, boolean unlinkDeathRecipient) {
+        synchronized (mLock) {
+            if (!mEnabled) {
+                return;
+            }
+            if (unlinkDeathRecipient) {
+                ApplicationStartInfoCompleteCallback callback = mCallbacks.get(uid);
+                if (callback != null) {
+                    callback.unlinkToDeath();
+                }
+            }
+            mCallbacks.remove(uid);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void forEachPackageLocked(
+            BiFunction<String, SparseArray<AppStartInfoContainer>, Integer> callback) {
+        if (callback != null) {
+            ArrayMap<String, SparseArray<AppStartInfoContainer>> map = mData.getMap();
+            for (int i = map.size() - 1; i >= 0; i--) {
+                switch (callback.apply(map.keyAt(i), map.valueAt(i))) {
+                    case FOREACH_ACTION_REMOVE_ITEM:
+                        map.removeAt(i);
+                        break;
+                    case FOREACH_ACTION_STOP_ITERATION:
+                        i = 0;
+                        break;
+                    case FOREACH_ACTION_NONE:
+                    default:
+                        break;
+                }
+            }
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void removePackageLocked(String packageName, int uid, boolean removeUid, int userId) {
+        ArrayMap<String, SparseArray<AppStartInfoContainer>> map = mData.getMap();
+        SparseArray<AppStartInfoContainer> array = map.get(packageName);
+        if (array == null) {
+            return;
+        }
+        if (userId == UserHandle.USER_ALL) {
+            mData.getMap().remove(packageName);
+        } else {
+            for (int i = array.size() - 1; i >= 0; i--) {
+                if (UserHandle.getUserId(array.keyAt(i)) == userId) {
+                    array.removeAt(i);
+                    break;
+                }
+            }
+            if (array.size() == 0) {
+                map.remove(packageName);
+            }
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void removeByUserIdLocked(final int userId) {
+        if (userId == UserHandle.USER_ALL) {
+            mData.getMap().clear();
+            return;
+        }
+        forEachPackageLocked(
+                (packageName, records) -> {
+                    for (int i = records.size() - 1; i >= 0; i--) {
+                        if (UserHandle.getUserId(records.keyAt(i)) == userId) {
+                            records.removeAt(i);
+                            break;
+                        }
+                    }
+                    return records.size() == 0 ? FOREACH_ACTION_REMOVE_ITEM : FOREACH_ACTION_NONE;
+                });
+    }
+
+    @VisibleForTesting
+    void onUserRemoved(int userId) {
+        synchronized (mLock) {
+            if (!mEnabled) {
+                return;
+            }
+            removeByUserIdLocked(userId);
+            schedulePersistProcessStartInfo(true);
+        }
+    }
+
+    @VisibleForTesting
+    void onPackageRemoved(String packageName, int uid, boolean allUsers) {
+        if (!mEnabled) {
+            return;
+        }
+        if (packageName != null) {
+            final boolean removeUid =
+                    TextUtils.isEmpty(mService.mPackageManagerInt.getNameForUid(uid));
+            synchronized (mLock) {
+                removePackageLocked(
+                        packageName,
+                        uid,
+                        removeUid,
+                        allUsers ? UserHandle.USER_ALL : UserHandle.getUserId(uid));
+                schedulePersistProcessStartInfo(true);
+            }
+        }
+    }
+
+    private void registerForUserRemoval() {
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_USER_REMOVED);
+        mService.mContext.registerReceiverForAllUsers(
+                new BroadcastReceiver() {
+                    @Override
+                    public void onReceive(Context context, Intent intent) {
+                        int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+                        if (userId < 1) return;
+                        onUserRemoved(userId);
+                    }
+                },
+                filter,
+                null,
+                mHandler);
+    }
+
+    private void registerForPackageRemoval() {
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        filter.addDataScheme("package");
+        mService.mContext.registerReceiverForAllUsers(
+                new BroadcastReceiver() {
+                    @Override
+                    public void onReceive(Context context, Intent intent) {
+                        boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false);
+                        if (replacing) {
+                            return;
+                        }
+                        int uid = intent.getIntExtra(Intent.EXTRA_UID, UserHandle.USER_NULL);
+                        boolean allUsers =
+                                intent.getBooleanExtra(Intent.EXTRA_REMOVED_FOR_ALL_USERS, false);
+                        onPackageRemoved(intent.getData().getSchemeSpecificPart(), uid, allUsers);
+                    }
+                },
+                filter,
+                null,
+                mHandler);
+    }
+
+    /**
+     * Load the existing {@link android.app.ApplicationStartInfo} records from persistent storage.
+     */
+    @VisibleForTesting
+    void loadExistingProcessStartInfo() {
+        if (!mEnabled) {
+            return;
+        }
+        if (!mProcStartInfoFile.canRead()) {
+            // If file can't be read, mark complete so we can begin accepting new records.
+            mAppStartInfoLoaded.set(true);
+            return;
+        }
+
+        FileInputStream fin = null;
+        try {
+            AtomicFile af = new AtomicFile(mProcStartInfoFile);
+            fin = af.openRead();
+            ProtoInputStream proto = new ProtoInputStream(fin);
+            for (int next = proto.nextField();
+                    next != ProtoInputStream.NO_MORE_FIELDS;
+                    next = proto.nextField()) {
+                switch (next) {
+                    case (int) AppsStartInfoProto.LAST_UPDATE_TIMESTAMP:
+                        synchronized (mLock) {
+                            mLastAppStartInfoPersistTimestamp =
+                                    proto.readLong(AppsStartInfoProto.LAST_UPDATE_TIMESTAMP);
+                        }
+                        break;
+                    case (int) AppsStartInfoProto.PACKAGES:
+                        loadPackagesFromProto(proto, next);
+                        break;
+                }
+            }
+        } catch (IOException | IllegalArgumentException | WireTypeMismatchException
+                | ClassNotFoundException e) {
+            Slog.w(TAG, "Error in loading historical app start info from persistent storage: " + e);
+        } finally {
+            if (fin != null) {
+                try {
+                    fin.close();
+                } catch (IOException e) {
+                }
+            }
+        }
+        mAppStartInfoLoaded.set(true);
+    }
+
+    private void loadPackagesFromProto(ProtoInputStream proto, long fieldId)
+            throws IOException, WireTypeMismatchException, ClassNotFoundException {
+        long token = proto.start(fieldId);
+        String pkgName = "";
+        for (int next = proto.nextField();
+                next != ProtoInputStream.NO_MORE_FIELDS;
+                next = proto.nextField()) {
+            switch (next) {
+                case (int) AppsStartInfoProto.Package.PACKAGE_NAME:
+                    pkgName = proto.readString(AppsStartInfoProto.Package.PACKAGE_NAME);
+                    break;
+                case (int) AppsStartInfoProto.Package.USERS:
+                    AppStartInfoContainer container =
+                            new AppStartInfoContainer(mAppStartInfoHistoryListSize);
+                    int uid = container.readFromProto(proto, AppsStartInfoProto.Package.USERS);
+                    synchronized (mLock) {
+                        mData.put(pkgName, uid, container);
+                    }
+                    break;
+            }
+        }
+        proto.end(token);
+    }
+
+    /** Persist the existing {@link android.app.ApplicationStartInfo} records to storage. */
+    @VisibleForTesting
+    void persistProcessStartInfo() {
+        if (!mEnabled) {
+            return;
+        }
+        AtomicFile af = new AtomicFile(mProcStartInfoFile);
+        FileOutputStream out = null;
+        long now = System.currentTimeMillis();
+        try {
+            out = af.startWrite();
+            ProtoOutputStream proto = new ProtoOutputStream(out);
+            proto.write(AppsStartInfoProto.LAST_UPDATE_TIMESTAMP, now);
+            synchronized (mLock) {
+                forEachPackageLocked(
+                        (packageName, records) -> {
+                            long token = proto.start(AppsStartInfoProto.PACKAGES);
+                            proto.write(AppsStartInfoProto.Package.PACKAGE_NAME, packageName);
+                            int uidArraySize = records.size();
+                            for (int j = 0; j < uidArraySize; j++) {
+                                try {
+                                    records.valueAt(j)
+                                        .writeToProto(proto, AppsStartInfoProto.Package.USERS);
+                                } catch (IOException e) {
+                                    Slog.w(TAG, "Unable to write app start info into persistent"
+                                            + "storage: " + e);
+                                }
+                            }
+                            proto.end(token);
+                            return AppStartInfoTracker.FOREACH_ACTION_NONE;
+                        });
+                mLastAppStartInfoPersistTimestamp = now;
+            }
+            proto.flush();
+            af.finishWrite(out);
+        } catch (IOException e) {
+            Slog.w(TAG, "Unable to write historical app start info into persistent storage: " + e);
+            af.failWrite(out);
+        }
+        synchronized (mLock) {
+            mAppStartInfoPersistTask = null;
+        }
+    }
+
+    /**
+     * Schedule a task to persist the {@link android.app.ApplicationStartInfo} records to storage.
+     */
+    @VisibleForTesting
+    void schedulePersistProcessStartInfo(boolean immediately) {
+        synchronized (mLock) {
+            if (!mEnabled) {
+                return;
+            }
+            if (mAppStartInfoPersistTask == null || immediately) {
+                if (mAppStartInfoPersistTask != null) {
+                    IoThread.getHandler().removeCallbacks(mAppStartInfoPersistTask);
+                }
+                mAppStartInfoPersistTask = this::persistProcessStartInfo;
+                IoThread.getHandler()
+                        .postDelayed(
+                                mAppStartInfoPersistTask,
+                                immediately ? 0 : APP_START_INFO_PERSIST_INTERVAL);
+            }
+        }
+    }
+
+    /** Helper function for testing only. */
+    @VisibleForTesting
+    void clearProcessStartInfo(boolean removeFile) {
+        synchronized (mLock) {
+            if (!mEnabled) {
+                return;
+            }
+            if (mAppStartInfoPersistTask != null) {
+                IoThread.getHandler().removeCallbacks(mAppStartInfoPersistTask);
+                mAppStartInfoPersistTask = null;
+            }
+            if (removeFile && mProcStartInfoFile != null) {
+                mProcStartInfoFile.delete();
+            }
+            mData.getMap().clear();
+        }
+    }
+
+    /**
+     * Helper functions for shell command.
+     * > adb shell dumpsys activity clear-start-info [package-name]
+     */
+    void clearHistoryProcessStartInfo(String packageName, int userId) {
+        if (!mEnabled) {
+            return;
+        }
+        Optional<Integer> appId = Optional.empty();
+        if (TextUtils.isEmpty(packageName)) {
+            synchronized (mLock) {
+                removeByUserIdLocked(userId);
+            }
+        } else {
+            final int uid =
+                    mService.mPackageManagerInt.getPackageUid(
+                            packageName, PackageManager.MATCH_ALL, userId);
+            appId = Optional.of(UserHandle.getAppId(uid));
+            synchronized (mLock) {
+                removePackageLocked(packageName, uid, true, userId);
+            }
+        }
+        schedulePersistProcessStartInfo(true);
+    }
+
+    /**
+     * Helper functions for shell command.
+     * > adb shell dumpsys activity start-info [package-name]
+     */
+    void dumpHistoryProcessStartInfo(PrintWriter pw, String packageName) {
+        if (!mEnabled) {
+            return;
+        }
+        pw.println("ACTIVITY MANAGER LRU PROCESSES (dumpsys activity start-info)");
+        SimpleDateFormat sdf = new SimpleDateFormat();
+        synchronized (mLock) {
+            pw.println("Last Timestamp of Persistence Into Persistent Storage: "
+                    + sdf.format(new Date(mLastAppStartInfoPersistTimestamp)));
+            if (TextUtils.isEmpty(packageName)) {
+                forEachPackageLocked((name, records) -> {
+                    dumpHistoryProcessStartInfoLocked(pw, "  ", name, records, sdf);
+                    return AppStartInfoTracker.FOREACH_ACTION_NONE;
+                });
+            } else {
+                SparseArray<AppStartInfoContainer> array = mData.getMap().get(packageName);
+                if (array != null) {
+                    dumpHistoryProcessStartInfoLocked(pw, "  ", packageName, array, sdf);
+                }
+            }
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void dumpHistoryProcessStartInfoLocked(PrintWriter pw, String prefix,
+            String packageName, SparseArray<AppStartInfoContainer> array,
+            SimpleDateFormat sdf) {
+        pw.println(prefix + "package: " + packageName);
+        int size = array.size();
+        for (int i = 0; i < size; i++) {
+            pw.println(prefix + "  Historical Process Start for userId=" + array.keyAt(i));
+            array.valueAt(i).dumpLocked(pw, prefix + "    ", sdf);
+        }
+    }
+
+    /** Convenience method to obtain timestamp of beginning of start.*/
+    private static long getStartTimestamp(ApplicationStartInfo startInfo) {
+        return startInfo.getStartupTimestamps().get(START_TIMESTAMP_LAUNCH);
+    }
+
+    /** A container class of (@link android.app.ApplicationStartInfo) */
+    final class AppStartInfoContainer {
+        private List<ApplicationStartInfo> mInfos; // Always kept sorted by first timestamp.
+        private int mMaxCapacity;
+        private int mUid;
+
+        AppStartInfoContainer(final int maxCapacity) {
+            mInfos = new ArrayList<ApplicationStartInfo>();
+            mMaxCapacity = maxCapacity;
+        }
+
+        @GuardedBy("mLock")
+        void getStartInfoLocked(
+                final int filterPid, final int maxNum, ArrayList<ApplicationStartInfo> results) {
+            results.addAll(mInfos.size() <= maxNum ? 0 : mInfos.size() - maxNum, mInfos);
+        }
+
+        @GuardedBy("mLock")
+        void addStartInfoLocked(ApplicationStartInfo info) {
+            int size = mInfos.size();
+            if (size >= mMaxCapacity) {
+                // Remove oldest record if size is over max capacity.
+                int oldestIndex = -1;
+                long oldestTimeStamp = Long.MAX_VALUE;
+                for (int i = 0; i < size; i++) {
+                    ApplicationStartInfo startInfo = mInfos.get(i);
+                    if (getStartTimestamp(startInfo) < oldestTimeStamp) {
+                        oldestTimeStamp = getStartTimestamp(startInfo);
+                        oldestIndex = i;
+                    }
+                }
+                if (oldestIndex >= 0) {
+                    mInfos.remove(oldestIndex);
+                }
+                mInfos.remove(0);
+            }
+            mInfos.add(info);
+            Collections.sort(mInfos, (a, b) ->
+                    Long.compare(getStartTimestamp(b), getStartTimestamp(a)));
+        }
+
+        @GuardedBy("mLock")
+        void addTimestampToStartLocked(int key, long timestampNs) {
+            int index = mInfos.size() - 1;
+            int startupState = mInfos.get(index).getStartupState();
+            if (startupState == ApplicationStartInfo.STARTUP_STATE_STARTED
+                    || key == ApplicationStartInfo.START_TIMESTAMP_FULLY_DRAWN) {
+                mInfos.get(index).addStartupTimestamp(key, timestampNs);
+            }
+        }
+
+        @GuardedBy("mLock")
+        void dumpLocked(PrintWriter pw, String prefix, SimpleDateFormat sdf) {
+            int size = mInfos.size();
+            for (int i = 0; i < size; i++) {
+                mInfos.get(i).dump(pw, prefix + "  ", "#" + i, sdf);
+            }
+        }
+
+        @GuardedBy("mLock")
+        void writeToProto(ProtoOutputStream proto, long fieldId) throws IOException {
+            long token = proto.start(fieldId);
+            proto.write(AppsStartInfoProto.Package.User.UID, mUid);
+            int size = mInfos.size();
+            for (int i = 0; i < size; i++) {
+                mInfos.get(i)
+                        .writeToProto(proto, AppsStartInfoProto.Package.User.APP_START_INFO);
+            }
+            proto.end(token);
+        }
+
+        int readFromProto(ProtoInputStream proto, long fieldId)
+                throws IOException, WireTypeMismatchException, ClassNotFoundException {
+            long token = proto.start(fieldId);
+            for (int next = proto.nextField();
+                    next != ProtoInputStream.NO_MORE_FIELDS;
+                    next = proto.nextField()) {
+                switch (next) {
+                    case (int) AppsStartInfoProto.Package.User.UID:
+                        mUid = proto.readInt(AppsStartInfoProto.Package.User.UID);
+                        break;
+                    case (int) AppsStartInfoProto.Package.User.APP_START_INFO:
+                        ApplicationStartInfo info = new ApplicationStartInfo();
+                        info.readFromProto(proto, AppsStartInfoProto.Package.User.APP_START_INFO);
+                        mInfos.add(info);
+                        break;
+                }
+            }
+            proto.end(token);
+            return mUid;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 0ab81a5..9e48b0a 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -121,6 +121,7 @@
 import com.android.server.power.stats.BatteryExternalStatsWorker;
 import com.android.server.power.stats.BatteryStatsImpl;
 import com.android.server.power.stats.BatteryUsageStatsProvider;
+import com.android.server.power.stats.CpuAggregatedPowerStatsProcessor;
 import com.android.server.power.stats.PowerStatsAggregator;
 import com.android.server.power.stats.PowerStatsScheduler;
 import com.android.server.power.stats.PowerStatsStore;
@@ -440,7 +441,9 @@
                 .trackUidStates(
                         AggregatedPowerStatsConfig.STATE_POWER,
                         AggregatedPowerStatsConfig.STATE_SCREEN,
-                        AggregatedPowerStatsConfig.STATE_PROCESS_STATE);
+                        AggregatedPowerStatsConfig.STATE_PROCESS_STATE)
+                .setProcessor(
+                        new CpuAggregatedPowerStatsProcessor(mPowerProfile, mCpuScalingPolicies));
         return config;
     }
 
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index f04198e..614caffe 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -494,6 +494,10 @@
     @GuardedBy("mService")
     final ProcessMap<AppZygote> mAppZygotes = new ProcessMap<AppZygote>();
 
+    /** Manages the {@link android.app.ApplicationStartInfo} records. */
+    @GuardedBy("mAppStartInfoTracker")
+    final AppStartInfoTracker mAppStartInfoTracker = new AppStartInfoTracker();
+
     /**
      * The currently running SDK sandbox processes for a uid.
      */
@@ -956,12 +960,14 @@
                         mSystemServerSocketForZygote.getFileDescriptor(),
                         EVENT_INPUT, this::handleZygoteMessages);
             }
+            mAppStartInfoTracker.init(mService);
             mAppExitInfoTracker.init(mService);
             mImperceptibleKillRunner = new ImperceptibleKillRunner(sKillThread.getLooper());
         }
     }
 
     void onSystemReady() {
+        mAppStartInfoTracker.onSystemReady();
         mAppExitInfoTracker.onSystemReady();
     }
 
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index a57a785..439fe0b 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -123,6 +123,7 @@
         "angle",
         "arc_next",
         "bluetooth",
+        "build",
         "biometrics_framework",
         "biometrics_integration",
         "camera_platform",
@@ -136,10 +137,12 @@
         "context_hub",
         "core_experiments_team_internal",
         "core_graphics",
+        "game",
         "haptics",
         "hardware_backed_security_mainline",
         "input",
         "machine_learning",
+        "mainline_modularization",
         "mainline_sdk",
         "media_audio",
         "media_drm",
@@ -152,9 +155,12 @@
         "platform_security",
         "power",
         "preload_safety",
+        "privacy_infra_policy",
+        "resource_manager",
         "responsible_apis",
         "rust",
         "safety_center",
+        "sensors",
         "system_performance",
         "test_suites",
         "text",
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 0dd579f..728bace 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -658,8 +658,8 @@
         mInjector.getUserJourneyLogger()
                 .logUserLifecycleEvent(userId, USER_LIFECYCLE_EVENT_UNLOCKING_USER,
                 EVENT_STATE_BEGIN);
-        // If the user key hasn't been unlocked yet, we cannot proceed.
-        if (!StorageManager.isUserKeyUnlocked(userId)) return false;
+        // If the user's CE storage hasn't been unlocked yet, we cannot proceed.
+        if (!StorageManager.isCeStorageUnlocked(userId)) return false;
         synchronized (mLock) {
             // Do not proceed if unexpected state or a stale user
             if (mStartedUsers.get(userId) != uss || uss.state != STATE_RUNNING_LOCKED) {
@@ -674,8 +674,8 @@
 
         // Call onBeforeUnlockUser on a worker thread that allows disk I/O
         FgThread.getHandler().post(() -> {
-            if (!StorageManager.isUserKeyUnlocked(userId)) {
-                Slogf.w(TAG, "User key got locked unexpectedly, leaving user locked.");
+            if (!StorageManager.isCeStorageUnlocked(userId)) {
+                Slogf.w(TAG, "User's CE storage got locked unexpectedly, leaving user locked.");
                 return;
             }
 
@@ -709,8 +709,8 @@
     private void finishUserUnlocked(final UserState uss) {
         final int userId = uss.mHandle.getIdentifier();
         EventLog.writeEvent(EventLogTags.UC_FINISH_USER_UNLOCKED, userId);
-        // Only keep marching forward if user is actually unlocked
-        if (!StorageManager.isUserKeyUnlocked(userId)) return;
+        // Only keep marching forward if the user's CE storage is unlocked.
+        if (!StorageManager.isCeStorageUnlocked(userId)) return;
         synchronized (mLock) {
             // Bail if we ended up with a stale user
             if (mStartedUsers.get(uss.mHandle.getIdentifier()) != uss) return;
@@ -796,8 +796,8 @@
         if (userInfo == null) {
             return;
         }
-        // Only keep marching forward if user is actually unlocked
-        if (!StorageManager.isUserKeyUnlocked(userId)) return;
+        // Only keep marching forward if the user's CE storage is unlocked.
+        if (!StorageManager.isCeStorageUnlocked(userId)) return;
 
         // Remember that we logged in
         mInjector.getUserManager().onUserLoggedIn(userId);
@@ -1330,7 +1330,7 @@
             }
             try {
                 Slogf.i(TAG, "Locking CE storage for user #" + userId);
-                mInjector.getStorageManager().lockUserKey(userId);
+                mInjector.getStorageManager().lockCeStorage(userId);
             } catch (RemoteException re) {
                 throw re.rethrowAsRuntimeException();
             }
@@ -1946,8 +1946,8 @@
         }
 
         UserState uss;
-        if (!StorageManager.isUserKeyUnlocked(userId)) {
-            // We always want to try to unlock the user key, even if the user is not started yet.
+        if (!StorageManager.isCeStorageUnlocked(userId)) {
+            // We always want to try to unlock CE storage, even if the user is not started yet.
             mLockPatternUtils.unlockUserKeyIfUnsecured(userId);
         }
         synchronized (mLock) {
@@ -2750,10 +2750,10 @@
                 case UserState.STATE_RUNNING_UNLOCKING:
                 case UserState.STATE_RUNNING_UNLOCKED:
                     return true;
-                // In the stopping/shutdown state return unlock state of the user key
+                // In the stopping/shutdown state, return unlock state of the user's CE storage.
                 case UserState.STATE_STOPPING:
                 case UserState.STATE_SHUTDOWN:
-                    return StorageManager.isUserKeyUnlocked(userId);
+                    return StorageManager.isCeStorageUnlocked(userId);
                 default:
                     return false;
             }
@@ -2762,10 +2762,10 @@
             switch (state.state) {
                 case UserState.STATE_RUNNING_UNLOCKED:
                     return true;
-                // In the stopping/shutdown state return unlock state of the user key
+                // In the stopping/shutdown state, return unlock state of the user's CE storage.
                 case UserState.STATE_STOPPING:
                 case UserState.STATE_SHUTDOWN:
-                    return StorageManager.isUserKeyUnlocked(userId);
+                    return StorageManager.isCeStorageUnlocked(userId);
                 default:
                     return false;
             }
diff --git a/services/core/java/com/android/server/audio/AdiDeviceState.java b/services/core/java/com/android/server/audio/AdiDeviceState.java
index 292fc14..51cb950 100644
--- a/services/core/java/com/android/server/audio/AdiDeviceState.java
+++ b/services/core/java/com/android/server/audio/AdiDeviceState.java
@@ -165,7 +165,8 @@
 
     @Override
     public String toString() {
-        return "type: " + mDeviceType + "internal type: " + mInternalDeviceType
+        return "type: " + mDeviceType
+                + " internal type: 0x" + Integer.toHexString(mInternalDeviceType)
                 + " addr: " + mDeviceAddress + " bt audio type: "
                 + AudioManager.audioDeviceCategoryToString(mAudioDeviceCategory)
                 + " enabled: " + mSAEnabled + " HT: " + mHasHeadTracker
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index eea3d38..2336753 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -1252,8 +1252,8 @@
     }
 
     /*package*/ void registerStrategyPreferredDevicesDispatcher(
-            @NonNull IStrategyPreferredDevicesDispatcher dispatcher) {
-        mDeviceInventory.registerStrategyPreferredDevicesDispatcher(dispatcher);
+            @NonNull IStrategyPreferredDevicesDispatcher dispatcher, boolean isPrivileged) {
+        mDeviceInventory.registerStrategyPreferredDevicesDispatcher(dispatcher, isPrivileged);
     }
 
     /*package*/ void unregisterStrategyPreferredDevicesDispatcher(
@@ -1262,8 +1262,8 @@
     }
 
     /*package*/ void registerStrategyNonDefaultDevicesDispatcher(
-            @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher) {
-        mDeviceInventory.registerStrategyNonDefaultDevicesDispatcher(dispatcher);
+            @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher, boolean isPrivileged) {
+        mDeviceInventory.registerStrategyNonDefaultDevicesDispatcher(dispatcher, isPrivileged);
     }
 
     /*package*/ void unregisterStrategyNonDefaultDevicesDispatcher(
@@ -1281,8 +1281,8 @@
     }
 
     /*package*/ void registerCapturePresetDevicesRoleDispatcher(
-            @NonNull ICapturePresetDevicesRoleDispatcher dispatcher) {
-        mDeviceInventory.registerCapturePresetDevicesRoleDispatcher(dispatcher);
+            @NonNull ICapturePresetDevicesRoleDispatcher dispatcher, boolean isPrivileged) {
+        mDeviceInventory.registerCapturePresetDevicesRoleDispatcher(dispatcher, isPrivileged);
     }
 
     /*package*/ void unregisterCapturePresetDevicesRoleDispatcher(
@@ -1290,6 +1290,11 @@
         mDeviceInventory.unregisterCapturePresetDevicesRoleDispatcher(dispatcher);
     }
 
+    /* package */ List<AudioDeviceAttributes> anonymizeAudioDeviceAttributesListUnchecked(
+            List<AudioDeviceAttributes> devices) {
+        return mAudioService.anonymizeAudioDeviceAttributesListUnchecked(devices);
+    }
+
     /*package*/ void registerCommunicationDeviceDispatcher(
             @NonNull ICommunicationDeviceDispatcher dispatcher) {
         mCommDevDispatchers.register(dispatcher);
@@ -2684,4 +2689,5 @@
     void clearDeviceInventory() {
         mDeviceInventory.clearDeviceInventory();
     }
+
 }
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index e08fdd6..d707689 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -121,6 +121,26 @@
     }
 
     /**
+     * Adds a new entry in mDeviceInventory if the AudioDeviceAttributes passed is an sink
+     * Bluetooth device and no corresponding entry already exists.
+     * @param ada the device to add if needed
+     */
+    void addAudioDeviceInInventoryIfNeeded(AudioDeviceAttributes ada) {
+        if (!AudioSystem.isBluetoothOutDevice(ada.getInternalType())) {
+            return;
+        }
+        synchronized (mDeviceInventoryLock) {
+            if (findDeviceStateForAudioDeviceAttributes(ada, ada.getType()) != null) {
+                return;
+            }
+            AdiDeviceState ads = new AdiDeviceState(
+                    ada.getType(), ada.getInternalType(), ada.getAddress());
+            mDeviceInventory.put(ads.getDeviceId(), ads);
+        }
+        mDeviceBroker.persistAudioDeviceSettings();
+    }
+
+    /**
      * Adds a new AdiDeviceState or updates the audio device cateogory of the matching
      * AdiDeviceState in the {@link AudioDeviceInventory#mDeviceInventory} list.
      * @param deviceState the device to update
@@ -904,9 +924,6 @@
             @NonNull List<AudioDeviceAttributes> devices) {
         int status = AudioSystem.ERROR;
         try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
-            AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
-                    "setPreferredDevicesForStrategy, strategy: " + strategy
-                            + " devices: " + devices)).printLog(TAG));
             status = setDevicesRoleForStrategy(
                     strategy, AudioSystem.DEVICE_ROLE_PREFERRED, devices, false /* internal */);
         }
@@ -932,10 +949,6 @@
         int status = AudioSystem.ERROR;
 
         try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
-            AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
-                            "removePreferredDevicesForStrategy, strategy: "
-                            + strategy)).printLog(TAG));
-
             status = clearDevicesRoleForStrategy(
                     strategy, AudioSystem.DEVICE_ROLE_PREFERRED, false /*internal */);
         }
@@ -954,10 +967,6 @@
         try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
             List<AudioDeviceAttributes> devices = new ArrayList<>();
             devices.add(device);
-
-            AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
-                            "setDeviceAsNonDefaultForStrategyAndSave, strategy: " + strategy
-                            + " device: " + device)).printLog(TAG));
             status = addDevicesRoleForStrategy(
                     strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices, false /* internal */);
         }
@@ -975,11 +984,6 @@
         try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
             List<AudioDeviceAttributes> devices = new ArrayList<>();
             devices.add(device);
-
-            AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
-                            "removeDeviceAsNonDefaultForStrategyAndSave, strategy: "
-                            + strategy + " devices: " + device)).printLog(TAG));
-
             status = removeDevicesRoleForStrategy(
                     strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices, false /* internal */);
         }
@@ -992,8 +996,8 @@
 
 
     /*package*/ void registerStrategyPreferredDevicesDispatcher(
-            @NonNull IStrategyPreferredDevicesDispatcher dispatcher) {
-        mPrefDevDispatchers.register(dispatcher);
+            @NonNull IStrategyPreferredDevicesDispatcher dispatcher, boolean isPrivileged) {
+        mPrefDevDispatchers.register(dispatcher, isPrivileged);
     }
 
     /*package*/ void unregisterStrategyPreferredDevicesDispatcher(
@@ -1002,8 +1006,8 @@
     }
 
     /*package*/ void registerStrategyNonDefaultDevicesDispatcher(
-            @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher) {
-        mNonDefDevDispatchers.register(dispatcher);
+            @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher, boolean isPrivileged) {
+        mNonDefDevDispatchers.register(dispatcher, isPrivileged);
     }
 
     /*package*/ void unregisterStrategyNonDefaultDevicesDispatcher(
@@ -1084,8 +1088,8 @@
     }
 
     /*package*/ void registerCapturePresetDevicesRoleDispatcher(
-            @NonNull ICapturePresetDevicesRoleDispatcher dispatcher) {
-        mDevRoleCapturePresetDispatchers.register(dispatcher);
+            @NonNull ICapturePresetDevicesRoleDispatcher dispatcher, boolean isPrivileged) {
+        mDevRoleCapturePresetDispatchers.register(dispatcher, isPrivileged);
     }
 
     /*package*/ void unregisterCapturePresetDevicesRoleDispatcher(
@@ -1414,6 +1418,8 @@
                     updateBluetoothPreferredModes_l(connect ? btDevice : null /*connectedDevice*/);
                     if (!connect) {
                         purgeDevicesRoles_l();
+                    } else {
+                        addAudioDeviceInInventoryIfNeeded(attributes);
                     }
                 }
                 mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.CONNECTED).record();
@@ -1702,6 +1708,7 @@
         setCurrentAudioRouteNameIfPossible(name, true /*fromA2dp*/);
 
         updateBluetoothPreferredModes_l(btInfo.mDevice /*connectedDevice*/);
+        addAudioDeviceInInventoryIfNeeded(ada);
     }
 
     static final int[] CAPTURE_PRESETS = new int[] {AudioSource.MIC, AudioSource.CAMCORDER,
@@ -2006,9 +2013,9 @@
         final int hearingAidVolIndex = mDeviceBroker.getVssVolumeForDevice(streamType,
                 AudioSystem.DEVICE_OUT_HEARING_AID);
         mDeviceBroker.postSetHearingAidVolumeIndex(hearingAidVolIndex, streamType);
-
-        mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes(
-                AudioSystem.DEVICE_OUT_HEARING_AID, address, name),
+        AudioDeviceAttributes ada = new AudioDeviceAttributes(
+                AudioSystem.DEVICE_OUT_HEARING_AID, address, name);
+        mAudioSystem.setDeviceConnectionState(ada,
                 AudioSystem.DEVICE_STATE_AVAILABLE,
                 AudioSystem.AUDIO_FORMAT_DEFAULT);
         mConnectedDevices.put(
@@ -2018,6 +2025,7 @@
         mDeviceBroker.postApplyVolumeOnDevice(streamType,
                 AudioSystem.DEVICE_OUT_HEARING_AID, "makeHearingAidDeviceAvailable");
         setCurrentAudioRouteNameIfPossible(name, false /*fromA2dp*/);
+        addAudioDeviceInInventoryIfNeeded(ada);
         new MediaMetrics.Item(mMetricsId + "makeHearingAidDeviceAvailable")
                 .set(MediaMetrics.Property.ADDRESS, address != null ? address : "")
                 .set(MediaMetrics.Property.DEVICE,
@@ -2128,6 +2136,7 @@
                             sensorUuid));
             mDeviceBroker.postAccessoryPlugMediaUnmute(device);
             setCurrentAudioRouteNameIfPossible(name, /*fromA2dp=*/false);
+            addAudioDeviceInInventoryIfNeeded(ada);
         }
 
         if (streamType == AudioSystem.STREAM_DEFAULT) {
@@ -2462,6 +2471,9 @@
         final int nbDispatchers = mPrefDevDispatchers.beginBroadcast();
         for (int i = 0; i < nbDispatchers; i++) {
             try {
+                if (!((Boolean) mPrefDevDispatchers.getBroadcastCookie(i))) {
+                    devices = mDeviceBroker.anonymizeAudioDeviceAttributesListUnchecked(devices);
+                }
                 mPrefDevDispatchers.getBroadcastItem(i).dispatchPrefDevicesChanged(
                         strategy, devices);
             } catch (RemoteException e) {
@@ -2475,6 +2487,9 @@
         final int nbDispatchers = mNonDefDevDispatchers.beginBroadcast();
         for (int i = 0; i < nbDispatchers; i++) {
             try {
+                if (!((Boolean) mNonDefDevDispatchers.getBroadcastCookie(i))) {
+                    devices = mDeviceBroker.anonymizeAudioDeviceAttributesListUnchecked(devices);
+                }
                 mNonDefDevDispatchers.getBroadcastItem(i).dispatchNonDefDevicesChanged(
                         strategy, devices);
             } catch (RemoteException e) {
@@ -2488,6 +2503,9 @@
         final int nbDispatchers = mDevRoleCapturePresetDispatchers.beginBroadcast();
         for (int i = 0; i < nbDispatchers; ++i) {
             try {
+                if (!((Boolean) mDevRoleCapturePresetDispatchers.getBroadcastCookie(i))) {
+                    devices = mDeviceBroker.anonymizeAudioDeviceAttributesListUnchecked(devices);
+                }
                 mDevRoleCapturePresetDispatchers.getBroadcastItem(i).dispatchDevicesRoleChanged(
                         capturePreset, role, devices);
             } catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 3243385..a8fa313 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -16,8 +16,8 @@
 
 package com.android.server.audio;
 
-import static android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED;
 import static android.app.BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT;
+import static android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED;
 import static android.media.AudioDeviceInfo.TYPE_BLE_HEADSET;
 import static android.media.AudioDeviceInfo.TYPE_BLE_SPEAKER;
 import static android.media.AudioDeviceInfo.TYPE_BLUETOOTH_A2DP;
@@ -36,6 +36,7 @@
 import static android.provider.Settings.Secure.VOLUME_HUSH_MUTE;
 import static android.provider.Settings.Secure.VOLUME_HUSH_OFF;
 import static android.provider.Settings.Secure.VOLUME_HUSH_VIBRATE;
+
 import static com.android.server.audio.SoundDoseHelper.ACTION_CHECK_MUSIC_ACTIVE;
 import static com.android.server.utils.EventLogger.Event.ALOGE;
 import static com.android.server.utils.EventLogger.Event.ALOGI;
@@ -149,6 +150,7 @@
 import android.media.projection.IMediaProjection;
 import android.media.projection.IMediaProjectionCallback;
 import android.media.projection.IMediaProjectionManager;
+import android.media.session.MediaSessionManager;
 import android.net.Uri;
 import android.os.Binder;
 import android.os.Build;
@@ -201,6 +203,7 @@
 import com.android.internal.os.SomeArgs;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.Preconditions;
+import com.android.media.audio.flags.Flags;
 import com.android.server.EventLogTags;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
@@ -307,6 +310,9 @@
     private final ContentResolver mContentResolver;
     private final AppOpsManager mAppOps;
 
+    /** do not use directly, use getMediaSessionManager() which handles lazy initialization */
+    @Nullable private volatile MediaSessionManager mMediaSessionManager;
+
     // the platform type affects volume and silent mode behavior
     private final int mPlatformType;
 
@@ -938,6 +944,8 @@
 
     private final SoundDoseHelper mSoundDoseHelper;
 
+    private final HardeningEnforcer mHardeningEnforcer;
+
     private final Object mSupportedSystemUsagesLock = new Object();
     @GuardedBy("mSupportedSystemUsagesLock")
     private @AttributeSystemUsage int[] mSupportedSystemUsages =
@@ -1312,6 +1320,8 @@
         mDisplayManager = context.getSystemService(DisplayManager.class);
 
         mMusicFxHelper = new MusicFxHelper(mContext, mAudioHandler);
+
+        mHardeningEnforcer = new HardeningEnforcer(mContext, isPlatformAutomotive());
     }
 
     private void initVolumeStreamStates() {
@@ -1381,7 +1391,6 @@
 
         // check on volume initialization
         checkVolumeRangeInitialization("AudioService()");
-
     }
 
     private SubscriptionManager.OnSubscriptionsChangedListener mSubscriptionChangedListener =
@@ -1394,6 +1403,14 @@
                 }
             };
 
+    private MediaSessionManager getMediaSessionManager() {
+        if (mMediaSessionManager == null) {
+            mMediaSessionManager = (MediaSessionManager) mContext
+                    .getSystemService(Context.MEDIA_SESSION_SERVICE);
+        }
+        return mMediaSessionManager;
+    }
+
     /**
      * Initialize intent receives and settings observers for this service.
      * Must be called after createStreamStates() as the handling of some events
@@ -2881,7 +2898,7 @@
     // IPC methods
     ///////////////////////////////////////////////////////////////////////////
     /**
-     * @see AudioManager#setPreferredDeviceForStrategy(AudioProductStrategy, AudioDeviceAttributes)
+     * @see AudioManager#setPreferredDevicesForStrategy(AudioProductStrategy, AudioDeviceAttributes)
      * @see AudioManager#setPreferredDevicesForStrategy(AudioProductStrategy,
      *                                                  List<AudioDeviceAttributes>)
      */
@@ -2891,8 +2908,11 @@
         if (devices == null) {
             return AudioSystem.ERROR;
         }
+
+        devices = retrieveBluetoothAddresses(devices);
+
         final String logString = String.format(
-                "setPreferredDeviceForStrategy u/pid:%d/%d strat:%d dev:%s",
+                "setPreferredDevicesForStrategy u/pid:%d/%d strat:%d dev:%s",
                 Binder.getCallingUid(), Binder.getCallingPid(), strategy,
                 devices.stream().map(e -> e.toString()).collect(Collectors.joining(",")));
         sDeviceLogger.enqueue(new EventLogger.StringEvent(logString).printLog(TAG));
@@ -2916,11 +2936,11 @@
         super.removePreferredDevicesForStrategy_enforcePermission();
 
         final String logString =
-                String.format("removePreferredDeviceForStrategy strat:%d", strategy);
+                String.format("removePreferredDevicesForStrategy strat:%d", strategy);
         sDeviceLogger.enqueue(new EventLogger.StringEvent(logString).printLog(TAG));
 
         final int status = mDeviceBroker.removePreferredDevicesForStrategySync(strategy);
-        if (status != AudioSystem.SUCCESS) {
+        if (status != AudioSystem.SUCCESS && status != AudioSystem.BAD_VALUE) {
             Log.e(TAG, String.format("Error %d in %s)", status, logString));
         }
         return status;
@@ -2948,7 +2968,7 @@
                     status, strategy));
             return new ArrayList<AudioDeviceAttributes>();
         } else {
-            return devices;
+            return anonymizeAudioDeviceAttributesList(devices);
         }
     }
 
@@ -2963,6 +2983,9 @@
                                                 @NonNull AudioDeviceAttributes device) {
         super.setDeviceAsNonDefaultForStrategy_enforcePermission();
         Objects.requireNonNull(device);
+
+        device = retrieveBluetoothAddress(device);
+
         final String logString = String.format(
                 "setDeviceAsNonDefaultForStrategy u/pid:%d/%d strat:%d dev:%s",
                 Binder.getCallingUid(), Binder.getCallingPid(), strategy, device.toString());
@@ -2989,6 +3012,9 @@
                                                    AudioDeviceAttributes device) {
         super.removeDeviceAsNonDefaultForStrategy_enforcePermission();
         Objects.requireNonNull(device);
+
+        device = retrieveBluetoothAddress(device);
+
         final String logString = String.format(
                 "removeDeviceAsNonDefaultForStrategy strat:%d dev:%s", strategy, device.toString());
         sDeviceLogger.enqueue(new EventLogger.StringEvent(logString).printLog(TAG));
@@ -2998,7 +3024,7 @@
         }
 
         final int status = mDeviceBroker.removeDeviceAsNonDefaultForStrategySync(strategy, device);
-        if (status != AudioSystem.SUCCESS) {
+        if (status != AudioSystem.SUCCESS && status != AudioSystem.BAD_VALUE) {
             Log.e(TAG, String.format("Error %d in %s)", status, logString));
         }
         return status;
@@ -3023,7 +3049,7 @@
                     status, strategy));
             return new ArrayList<AudioDeviceAttributes>();
         } else {
-            return devices;
+            return anonymizeAudioDeviceAttributesList(devices);
         }
     }
 
@@ -3036,7 +3062,8 @@
             return;
         }
         enforceModifyAudioRoutingPermission();
-        mDeviceBroker.registerStrategyPreferredDevicesDispatcher(dispatcher);
+        mDeviceBroker.registerStrategyPreferredDevicesDispatcher(
+                dispatcher, isBluetoothPrividged());
     }
 
     /** @see AudioManager#removeOnPreferredDevicesForStrategyChangedListener(
@@ -3060,7 +3087,8 @@
             return;
         }
         enforceModifyAudioRoutingPermission();
-        mDeviceBroker.registerStrategyNonDefaultDevicesDispatcher(dispatcher);
+        mDeviceBroker.registerStrategyNonDefaultDevicesDispatcher(
+                dispatcher, isBluetoothPrividged());
     }
 
     /** @see AudioManager#removeOnNonDefaultDevicesForStrategyChangedListener(
@@ -3076,7 +3104,7 @@
     }
 
     /**
-     * @see AudioManager#setPreferredDeviceForCapturePreset(int, AudioDeviceAttributes)
+     * @see AudioManager#setPreferredDevicesForCapturePreset(int, AudioDeviceAttributes)
      */
     public int setPreferredDevicesForCapturePreset(
             int capturePreset, List<AudioDeviceAttributes> devices) {
@@ -3095,6 +3123,8 @@
             return AudioSystem.ERROR;
         }
 
+        devices = retrieveBluetoothAddresses(devices);
+
         final int status = mDeviceBroker.setPreferredDevicesForCapturePresetSync(
                 capturePreset, devices);
         if (status != AudioSystem.SUCCESS) {
@@ -3114,7 +3144,7 @@
         sDeviceLogger.enqueue(new EventLogger.StringEvent(logString).printLog(TAG));
 
         final int status = mDeviceBroker.clearPreferredDevicesForCapturePresetSync(capturePreset);
-        if (status != AudioSystem.SUCCESS) {
+        if (status != AudioSystem.SUCCESS && status != AudioSystem.BAD_VALUE) {
             Log.e(TAG, String.format("Error %d in %s", status, logString));
         }
         return status;
@@ -3141,7 +3171,7 @@
                     status, capturePreset));
             return new ArrayList<AudioDeviceAttributes>();
         } else {
-            return devices;
+            return anonymizeAudioDeviceAttributesList(devices);
         }
     }
 
@@ -3155,7 +3185,8 @@
             return;
         }
         enforceModifyAudioRoutingPermission();
-        mDeviceBroker.registerCapturePresetDevicesRoleDispatcher(dispatcher);
+        mDeviceBroker.registerCapturePresetDevicesRoleDispatcher(
+                dispatcher, isBluetoothPrividged());
     }
 
     /**
@@ -3175,7 +3206,9 @@
     public @NonNull ArrayList<AudioDeviceAttributes> getDevicesForAttributes(
             @NonNull AudioAttributes attributes) {
         enforceQueryStateOrModifyRoutingPermission();
-        return getDevicesForAttributesInt(attributes, false /* forVolume */);
+
+        return new ArrayList<AudioDeviceAttributes>(anonymizeAudioDeviceAttributesList(
+                getDevicesForAttributesInt(attributes, false /* forVolume */)));
     }
 
     /** @see AudioManager#getAudioDevicesForAttributes(AudioAttributes)
@@ -3185,7 +3218,8 @@
      */
     public @NonNull ArrayList<AudioDeviceAttributes> getDevicesForAttributesUnprotected(
             @NonNull AudioAttributes attributes) {
-        return getDevicesForAttributesInt(attributes, false /* forVolume */);
+        return new ArrayList<AudioDeviceAttributes>(anonymizeAudioDeviceAttributesList(
+                getDevicesForAttributesInt(attributes, false /* forVolume */)));
     }
 
     /**
@@ -3405,6 +3439,10 @@
      * Part of service interface, check permissions here */
     public void adjustStreamVolumeWithAttribution(int streamType, int direction, int flags,
             String callingPackage, String attributionTag) {
+        if (mHardeningEnforcer.blockVolumeMethod(
+                HardeningEnforcer.METHOD_AUDIO_MANAGER_ADJUST_STREAM_VOLUME)) {
+            return;
+        }
         if ((streamType == AudioManager.STREAM_ACCESSIBILITY) && !canChangeAccessibilityVolume()) {
             Log.w(TAG, "Trying to call adjustStreamVolume() for a11y without"
                     + "CHANGE_ACCESSIBILITY_VOLUME / callingPackage=" + callingPackage);
@@ -4181,6 +4219,10 @@
      * Part of service interface, check permissions here */
     public void setStreamVolumeWithAttribution(int streamType, int index, int flags,
             String callingPackage, String attributionTag) {
+        if (mHardeningEnforcer.blockVolumeMethod(
+                HardeningEnforcer.METHOD_AUDIO_MANAGER_SET_STREAM_VOLUME)) {
+            return;
+        }
         setStreamVolumeWithAttributionInt(streamType, index, flags, /*device*/ null,
                 callingPackage, attributionTag);
     }
@@ -5033,6 +5075,7 @@
     /** @see AudioManager#setMasterMute(boolean, int) */
     public void setMasterMute(boolean mute, int flags, String callingPackage, int userId,
             String attributionTag) {
+
         super.setMasterMute_enforcePermission();
 
         setMasterMuteInternal(mute, flags, callingPackage,
@@ -5398,6 +5441,10 @@
     }
 
     public void setRingerModeExternal(int ringerMode, String caller) {
+        if (mHardeningEnforcer.blockVolumeMethod(
+                HardeningEnforcer.METHOD_AUDIO_MANAGER_SET_RINGER_MODE)) {
+            return;
+        }
         if (isAndroidNPlus(caller) && wouldToggleZenMode(ringerMode)
                 && !mNm.isNotificationPolicyAccessGrantedForPackage(caller)) {
             throw new SecurityException("Not allowed to change Do Not Disturb state");
@@ -6150,6 +6197,35 @@
                 AudioDeviceVolumeManager.ADJUST_MODE_NORMAL);
     }
 
+    /**
+      * @see AudioManager#adjustVolume(int, int)
+      * This method is redirected from AudioManager to AudioService for API hardening rules
+      * enforcement then to MediaSession for implementation.
+      */
+    @Override
+    public void adjustVolume(int direction, int flags) {
+        if (mHardeningEnforcer.blockVolumeMethod(
+                HardeningEnforcer.METHOD_AUDIO_MANAGER_ADJUST_VOLUME)) {
+            return;
+        }
+        getMediaSessionManager().dispatchAdjustVolume(AudioManager.USE_DEFAULT_STREAM_TYPE,
+                    direction, flags);
+    }
+
+    /**
+     * @see AudioManager#adjustSuggestedStreamVolume(int, int, int)
+     * This method is redirected from AudioManager to AudioService for API hardening rules
+     * enforcement then to MediaSession for implementation.
+     */
+    @Override
+    public void adjustSuggestedStreamVolume(int direction, int suggestedStreamType, int flags) {
+        if (mHardeningEnforcer.blockVolumeMethod(
+                HardeningEnforcer.METHOD_AUDIO_MANAGER_ADJUST_SUGGESTED_STREAM_VOLUME)) {
+            return;
+        }
+        getMediaSessionManager().dispatchAdjustVolume(suggestedStreamType, direction, flags);
+    }
+
     /** @see AudioManager#setStreamVolumeForUid(int, int, int, String, int, int, int) */
     @Override
     public void setStreamVolumeForUid(int streamType, int index, int flags,
@@ -7390,6 +7466,8 @@
         Objects.requireNonNull(device);
         AudioManager.enforceValidVolumeBehavior(deviceVolumeBehavior);
 
+        device = retrieveBluetoothAddress(device);
+
         sVolumeLogger.enqueue(new EventLogger.StringEvent("setDeviceVolumeBehavior: dev:"
                 + AudioSystem.getOutputDeviceName(device.getInternalType()) + " addr:"
                 + device.getAddress() + " behavior:"
@@ -7473,6 +7551,8 @@
         // verify parameters
         Objects.requireNonNull(device);
 
+        device = retrieveBluetoothAddress(device);
+
         return getDeviceVolumeBehaviorInt(device);
     }
 
@@ -7547,9 +7627,12 @@
     /**
      * see AudioManager.setWiredDeviceConnectionState()
      */
-    public void setWiredDeviceConnectionState(AudioDeviceAttributes attributes,
+    public void setWiredDeviceConnectionState(@NonNull AudioDeviceAttributes attributes,
             @ConnectionState int state, String caller) {
         super.setWiredDeviceConnectionState_enforcePermission();
+        Objects.requireNonNull(attributes);
+
+        attributes = retrieveBluetoothAddress(attributes);
 
         if (state != CONNECTION_STATE_CONNECTED
                 && state != CONNECTION_STATE_DISCONNECTED) {
@@ -7590,6 +7673,9 @@
             boolean connected) {
         Objects.requireNonNull(device);
         enforceModifyAudioRoutingPermission();
+
+        device = retrieveBluetoothAddress(device);
+
         mDeviceBroker.setTestDeviceConnectionState(device,
                 connected ? CONNECTION_STATE_CONNECTED : CONNECTION_STATE_DISCONNECTED);
         // simulate a routing update from native
@@ -10422,6 +10508,103 @@
         mSpatializerHelper.setFeatureEnabled(mHasSpatializerEffect);
     }
 
+    private boolean isBluetoothPrividged() {
+        if (!Flags.bluetoothMacAddressAnonymization()) {
+            return true;
+        }
+        return PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission(
+                android.Manifest.permission.BLUETOOTH_CONNECT)
+                || Binder.getCallingUid() == Process.SYSTEM_UID;
+    }
+
+    List<AudioDeviceAttributes> retrieveBluetoothAddresses(List<AudioDeviceAttributes> devices) {
+        if (isBluetoothPrividged()) {
+            return devices;
+        }
+
+        List<AudioDeviceAttributes> checkedDevices = new ArrayList<AudioDeviceAttributes>();
+        for (AudioDeviceAttributes ada : devices) {
+            if (ada == null) {
+                continue;
+            }
+            checkedDevices.add(retrieveBluetoothAddressUncheked(ada));
+        }
+        return checkedDevices;
+    }
+
+    AudioDeviceAttributes retrieveBluetoothAddress(@NonNull AudioDeviceAttributes ada) {
+        if (isBluetoothPrividged()) {
+            return ada;
+        }
+        return retrieveBluetoothAddressUncheked(ada);
+    }
+
+    AudioDeviceAttributes retrieveBluetoothAddressUncheked(@NonNull AudioDeviceAttributes ada) {
+        Objects.requireNonNull(ada);
+        if (AudioSystem.isBluetoothDevice(ada.getInternalType())) {
+            String anonymizedAddress = anonymizeBluetoothAddress(ada.getAddress());
+            for (AdiDeviceState ads : mDeviceBroker.getImmutableDeviceInventory()) {
+                if (!(AudioSystem.isBluetoothDevice(ads.getInternalDeviceType())
+                        && (ada.getInternalType() == ads.getInternalDeviceType())
+                        && anonymizedAddress.equals(anonymizeBluetoothAddress(
+                                ads.getDeviceAddress())))) {
+                    continue;
+                }
+                ada.setAddress(ads.getDeviceAddress());
+                break;
+            }
+        }
+        return ada;
+    }
+
+    /**
+     * Convert a Bluetooth MAC address to an anonymized one when exposed to a non privileged app
+     * Must match the implementation of BluetoothUtils.toAnonymizedAddress()
+     * @param address Mac address to be anonymized
+     * @return anonymized mac address
+     */
+    static String anonymizeBluetoothAddress(String address) {
+        if (address == null || address.length() != "AA:BB:CC:DD:EE:FF".length()) {
+            return null;
+        }
+        return "XX:XX:XX:XX" + address.substring("XX:XX:XX:XX".length());
+    }
+
+    private List<AudioDeviceAttributes> anonymizeAudioDeviceAttributesList(
+                List<AudioDeviceAttributes> devices) {
+        if (isBluetoothPrividged()) {
+            return devices;
+        }
+        return anonymizeAudioDeviceAttributesListUnchecked(devices);
+    }
+
+    /* package */ List<AudioDeviceAttributes> anonymizeAudioDeviceAttributesListUnchecked(
+            List<AudioDeviceAttributes> devices) {
+        List<AudioDeviceAttributes> anonymizedDevices = new ArrayList<AudioDeviceAttributes>();
+        for (AudioDeviceAttributes ada : devices) {
+            anonymizedDevices.add(anonymizeAudioDeviceAttributesUnchecked(ada));
+        }
+        return anonymizedDevices;
+    }
+
+    private AudioDeviceAttributes anonymizeAudioDeviceAttributesUnchecked(
+            AudioDeviceAttributes ada) {
+        if (!AudioSystem.isBluetoothDevice(ada.getInternalType())) {
+            return ada;
+        }
+        AudioDeviceAttributes res = new AudioDeviceAttributes(ada);
+        res.setAddress(anonymizeBluetoothAddress(ada.getAddress()));
+        return res;
+    }
+
+    private AudioDeviceAttributes anonymizeAudioDeviceAttributes(AudioDeviceAttributes ada) {
+        if (isBluetoothPrividged()) {
+            return ada;
+        }
+
+        return anonymizeAudioDeviceAttributesUnchecked(ada);
+    }
+
     //==========================================================================================
 
     // camera sound is forced if any of the resources corresponding to one active SIM
@@ -10469,13 +10652,16 @@
         Objects.requireNonNull(usages);
         Objects.requireNonNull(device);
         enforceModifyAudioRoutingPermission();
+
+        final AudioDeviceAttributes ada = retrieveBluetoothAddress(device);
+
         if (timeOutMs <= 0 || usages.length == 0) {
             throw new IllegalArgumentException("Invalid timeOutMs/usagesToMute");
         }
         Log.i(TAG, "muteAwaitConnection dev:" + device + " timeOutMs:" + timeOutMs
                 + " usages:" + Arrays.toString(usages));
 
-        if (mDeviceBroker.isDeviceConnected(device)) {
+        if (mDeviceBroker.isDeviceConnected(ada)) {
             // not throwing an exception as there could be a race between a connection (server-side,
             // notification of connection in flight) and a mute operation (client-side)
             Log.i(TAG, "muteAwaitConnection ignored, device (" + device + ") already connected");
@@ -10487,12 +10673,19 @@
                         + mMutingExpectedDevice);
                 throw new IllegalStateException("muteAwaitConnection already in progress");
             }
-            mMutingExpectedDevice = device;
+            mMutingExpectedDevice = ada;
             mMutedUsagesAwaitingConnection = usages;
-            mPlaybackMonitor.muteAwaitConnection(usages, device, timeOutMs);
+            mPlaybackMonitor.muteAwaitConnection(usages, ada, timeOutMs);
         }
-        dispatchMuteAwaitConnection(cb -> { try {
-            cb.dispatchOnMutedUntilConnection(device, usages); } catch (RemoteException e) { } });
+        dispatchMuteAwaitConnection((cb, isPrivileged) -> {
+            try {
+                AudioDeviceAttributes dev = ada;
+                if (!isPrivileged) {
+                    dev = anonymizeAudioDeviceAttributesUnchecked(ada);
+                }
+                cb.dispatchOnMutedUntilConnection(dev, usages);
+            } catch (RemoteException e) { }
+        });
     }
 
     @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
@@ -10501,7 +10694,7 @@
         super.getMutingExpectedDevice_enforcePermission();
 
         synchronized (mMuteAwaitConnectionLock) {
-            return mMutingExpectedDevice;
+            return anonymizeAudioDeviceAttributes(mMutingExpectedDevice);
         }
     }
 
@@ -10510,6 +10703,9 @@
     public void cancelMuteAwaitConnection(@NonNull AudioDeviceAttributes device) {
         Objects.requireNonNull(device);
         enforceModifyAudioRoutingPermission();
+
+        final AudioDeviceAttributes ada = retrieveBluetoothAddress(device);
+
         Log.i(TAG, "cancelMuteAwaitConnection for device:" + device);
         final int[] mutedUsages;
         synchronized (mMuteAwaitConnectionLock) {
@@ -10519,7 +10715,7 @@
                 Log.i(TAG, "cancelMuteAwaitConnection ignored, no expected device");
                 return;
             }
-            if (!device.equalTypeAddress(mMutingExpectedDevice)) {
+            if (!ada.equalTypeAddress(mMutingExpectedDevice)) {
                 Log.e(TAG, "cancelMuteAwaitConnection ignored, got " + device
                         + "] but expected device is" + mMutingExpectedDevice);
                 throw new IllegalStateException("cancelMuteAwaitConnection for wrong device");
@@ -10529,8 +10725,14 @@
             mMutedUsagesAwaitingConnection = null;
             mPlaybackMonitor.cancelMuteAwaitConnection("cancelMuteAwaitConnection dev:" + device);
         }
-        dispatchMuteAwaitConnection(cb -> { try { cb.dispatchOnUnmutedEvent(
-                    AudioManager.MuteAwaitConnectionCallback.EVENT_CANCEL, device, mutedUsages);
+        dispatchMuteAwaitConnection((cb, isPrivileged) -> {
+            try {
+                AudioDeviceAttributes dev = ada;
+                if (!isPrivileged) {
+                    dev = anonymizeAudioDeviceAttributesUnchecked(ada);
+                }
+                cb.dispatchOnUnmutedEvent(
+                        AudioManager.MuteAwaitConnectionCallback.EVENT_CANCEL, dev, mutedUsages);
             } catch (RemoteException e) { } });
     }
 
@@ -10544,7 +10746,7 @@
         super.registerMuteAwaitConnectionDispatcher_enforcePermission();
 
         if (register) {
-            mMuteAwaitConnectionDispatchers.register(cb);
+            mMuteAwaitConnectionDispatchers.register(cb, isBluetoothPrividged());
         } else {
             mMuteAwaitConnectionDispatchers.unregister(cb);
         }
@@ -10568,8 +10770,14 @@
             mPlaybackMonitor.cancelMuteAwaitConnection(
                     "checkMuteAwaitConnection device " + device + " connected, unmuting");
         }
-        dispatchMuteAwaitConnection(cb -> { try { cb.dispatchOnUnmutedEvent(
-                AudioManager.MuteAwaitConnectionCallback.EVENT_CONNECTION, device, mutedUsages);
+        dispatchMuteAwaitConnection((cb, isPrivileged) -> {
+            try {
+                AudioDeviceAttributes ada = device;
+                if (!isPrivileged) {
+                    ada = anonymizeAudioDeviceAttributesUnchecked(device);
+                }
+                cb.dispatchOnUnmutedEvent(AudioManager.MuteAwaitConnectionCallback.EVENT_CONNECTION,
+                        ada, mutedUsages);
             } catch (RemoteException e) { } });
     }
 
@@ -10589,7 +10797,8 @@
             mMutingExpectedDevice = null;
             mMutedUsagesAwaitingConnection = null;
         }
-        dispatchMuteAwaitConnection(cb -> { try {
+        dispatchMuteAwaitConnection((cb, isPrivileged) -> {
+            try {
                 cb.dispatchOnUnmutedEvent(
                         AudioManager.MuteAwaitConnectionCallback.EVENT_TIMEOUT,
                         timedOutDevice, mutedUsages);
@@ -10597,13 +10806,14 @@
     }
 
     private void dispatchMuteAwaitConnection(
-            java.util.function.Consumer<IMuteAwaitConnectionCallback> callback) {
+            java.util.function.BiConsumer<IMuteAwaitConnectionCallback, Boolean> callback) {
         final int nbDispatchers = mMuteAwaitConnectionDispatchers.beginBroadcast();
         // lazy initialization as errors unlikely
         ArrayList<IMuteAwaitConnectionCallback> errorList = null;
         for (int i = 0; i < nbDispatchers; i++) {
             try {
-                callback.accept(mMuteAwaitConnectionDispatchers.getBroadcastItem(i));
+                callback.accept(mMuteAwaitConnectionDispatchers.getBroadcastItem(i),
+                        (Boolean) mMuteAwaitConnectionDispatchers.getBroadcastCookie(i));
             } catch (Exception e) {
                 if (errorList == null) {
                     errorList = new ArrayList<>(1);
@@ -13243,6 +13453,9 @@
             @NonNull AudioDeviceAttributes device, @IntRange(from = 0) long delayMillis) {
         Objects.requireNonNull(device, "device must not be null");
         enforceModifyAudioRoutingPermission();
+
+        device = retrieveBluetoothAddress(device);
+
         final String getterKey = "additional_output_device_delay="
                 + device.getInternalType() + "," + device.getAddress(); // "getter" key as an id.
         final String setterKey = getterKey + "," + delayMillis;     // append the delay for setter
@@ -13263,6 +13476,9 @@
     @IntRange(from = 0)
     public long getAdditionalOutputDeviceDelay(@NonNull AudioDeviceAttributes device) {
         Objects.requireNonNull(device, "device must not be null");
+
+        device = retrieveBluetoothAddress(device);
+
         final String key = "additional_output_device_delay";
         final String reply = AudioSystem.getParameters(
                 key + "=" + device.getInternalType() + "," + device.getAddress());
@@ -13290,6 +13506,9 @@
     @IntRange(from = 0)
     public long getMaxAdditionalOutputDeviceDelay(@NonNull AudioDeviceAttributes device) {
         Objects.requireNonNull(device, "device must not be null");
+
+        device = retrieveBluetoothAddress(device);
+
         final String key = "max_additional_output_device_delay";
         final String reply = AudioSystem.getParameters(
                 key + "=" + device.getInternalType() + "," + device.getAddress());
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index 6af409e..7d7e6d0 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -44,7 +44,9 @@
 
 import java.io.PrintWriter;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 
 /**
@@ -66,6 +68,8 @@
 
     // Bluetooth headset device
     private @Nullable BluetoothDevice mBluetoothHeadsetDevice;
+    private final Map<BluetoothDevice, AudioDeviceAttributes> mResolvedScoAudioDevices =
+            new HashMap<>();
 
     private @Nullable BluetoothHearingAid mHearingAid;
 
@@ -590,7 +594,16 @@
         if (mBluetoothHeadsetDevice == null) {
             return null;
         }
-        return btHeadsetDeviceToAudioDevice(mBluetoothHeadsetDevice);
+        return getHeadsetAudioDevice(mBluetoothHeadsetDevice);
+    }
+
+    private @NonNull AudioDeviceAttributes getHeadsetAudioDevice(BluetoothDevice btDevice) {
+        AudioDeviceAttributes deviceAttr = mResolvedScoAudioDevices.get(btDevice);
+        if (deviceAttr != null) {
+            // Returns the cached device attributes so that it is consistent as the previous one.
+            return deviceAttr;
+        }
+        return btHeadsetDeviceToAudioDevice(btDevice);
     }
 
     private static AudioDeviceAttributes btHeadsetDeviceToAudioDevice(BluetoothDevice btDevice) {
@@ -628,7 +641,7 @@
             return true;
         }
         int inDevice = AudioSystem.DEVICE_IN_BLUETOOTH_SCO_HEADSET;
-        AudioDeviceAttributes audioDevice =  btHeadsetDeviceToAudioDevice(btDevice);
+        AudioDeviceAttributes audioDevice = btHeadsetDeviceToAudioDevice(btDevice);
         boolean result = false;
         if (isActive) {
             result |= mDeviceBroker.handleDeviceConnection(audioDevice, isActive, btDevice);
@@ -648,6 +661,13 @@
         result = mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes(
                         inDevice, audioDevice.getAddress(), audioDevice.getName()),
                 isActive, btDevice) && result;
+        if (result) {
+            if (isActive) {
+                mResolvedScoAudioDevices.put(btDevice, audioDevice);
+            } else {
+                mResolvedScoAudioDevices.remove(btDevice);
+            }
+        }
         return result;
     }
 
diff --git a/services/core/java/com/android/server/audio/HardeningEnforcer.java b/services/core/java/com/android/server/audio/HardeningEnforcer.java
new file mode 100644
index 0000000..c7556da
--- /dev/null
+++ b/services/core/java/com/android/server/audio/HardeningEnforcer.java
@@ -0,0 +1,109 @@
+/*
+ * 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
+ *
+ *      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.audio;
+
+import static com.android.media.audio.flags.Flags.autoPublicVolumeApiHardening;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.media.AudioManager;
+import android.os.Binder;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Log;
+
+/**
+ * Class to encapsulate all audio API hardening operations
+ */
+public class HardeningEnforcer {
+
+    private static final String TAG = "AS.HardeningEnforcer";
+
+    final Context mContext;
+    final boolean mIsAutomotive;
+
+    /**
+     * Matches calls from {@link AudioManager#setStreamVolume(int, int, int)}
+     */
+    public static final int METHOD_AUDIO_MANAGER_SET_STREAM_VOLUME = 100;
+    /**
+     * Matches calls from {@link AudioManager#adjustVolume(int, int)}
+     */
+    public static final int METHOD_AUDIO_MANAGER_ADJUST_VOLUME = 101;
+    /**
+     * Matches calls from {@link AudioManager#adjustSuggestedStreamVolume(int, int, int)}
+     */
+    public static final int METHOD_AUDIO_MANAGER_ADJUST_SUGGESTED_STREAM_VOLUME = 102;
+    /**
+     * Matches calls from {@link AudioManager#adjustStreamVolume(int, int, int)}
+     */
+    public static final int METHOD_AUDIO_MANAGER_ADJUST_STREAM_VOLUME = 103;
+    /**
+     * Matches calls from {@link AudioManager#setRingerMode(int)}
+     */
+    public static final int METHOD_AUDIO_MANAGER_SET_RINGER_MODE = 200;
+
+    public HardeningEnforcer(Context ctxt, boolean isAutomotive) {
+        mContext = ctxt;
+        mIsAutomotive = isAutomotive;
+    }
+
+    /**
+     * Checks whether the call in the current thread should be allowed or blocked
+     * @param volumeMethod name of the method to check, for logging purposes
+     * @return false if the method call is allowed, true if it should be a no-op
+     */
+    protected boolean blockVolumeMethod(int volumeMethod) {
+        // for Auto, volume methods require MODIFY_AUDIO_SETTINGS_PRIVILEGED
+        if (mIsAutomotive) {
+            if (!autoPublicVolumeApiHardening()) {
+                // automotive hardening flag disabled, no blocking on auto
+                return false;
+            }
+            if (mContext.checkCallingOrSelfPermission(
+                    Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+                    == PackageManager.PERMISSION_GRANTED) {
+                return false;
+            }
+            if (Binder.getCallingUid() < UserHandle.AID_APP_START) {
+                return false;
+            }
+            // TODO metrics?
+            // TODO log for audio dumpsys?
+            Log.e(TAG, "Preventing volume method " + volumeMethod + " for "
+                    + getPackNameForUid(Binder.getCallingUid()));
+            return true;
+        }
+        // not blocking
+        return false;
+    }
+
+    private String getPackNameForUid(int uid) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            final String[] names = mContext.getPackageManager().getPackagesForUid(uid);
+            if (names == null
+                    || names.length == 0
+                    || TextUtils.isEmpty(names[0])) {
+                return "[" + uid + "]";
+            }
+            return names[0];
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/biometrics/AuthService.java b/services/core/java/com/android/server/biometrics/AuthService.java
index 1760bb3..4538cad 100644
--- a/services/core/java/com/android/server/biometrics/AuthService.java
+++ b/services/core/java/com/android/server/biometrics/AuthService.java
@@ -41,7 +41,6 @@
 import android.hardware.biometrics.ComponentInfoInternal;
 import android.hardware.biometrics.IAuthService;
 import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback;
-import android.hardware.biometrics.IBiometricPromptStatusListener;
 import android.hardware.biometrics.IBiometricService;
 import android.hardware.biometrics.IBiometricServiceReceiver;
 import android.hardware.biometrics.IInvalidationCallback;
@@ -358,18 +357,6 @@
         }
 
         @Override
-        public void registerBiometricPromptStatusListener(
-                IBiometricPromptStatusListener listener) throws RemoteException {
-            checkInternalPermission();
-            final long identity = Binder.clearCallingIdentity();
-            try {
-                mBiometricService.registerBiometricPromptStatusListener(listener);
-            } finally {
-                Binder.restoreCallingIdentity(identity);
-            }
-        }
-
-        @Override
         public void invalidateAuthenticatorIds(int userId, int fromSensorId,
                 IInvalidationCallback callback) throws RemoteException {
             checkInternalPermission();
diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java
index 9569f23..1898b80 100644
--- a/services/core/java/com/android/server/biometrics/BiometricService.java
+++ b/services/core/java/com/android/server/biometrics/BiometricService.java
@@ -41,7 +41,6 @@
 import android.hardware.biometrics.BiometricPrompt;
 import android.hardware.biometrics.IBiometricAuthenticator;
 import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback;
-import android.hardware.biometrics.IBiometricPromptStatusListener;
 import android.hardware.biometrics.IBiometricSensorReceiver;
 import android.hardware.biometrics.IBiometricService;
 import android.hardware.biometrics.IBiometricServiceReceiver;
@@ -89,7 +88,6 @@
 import java.util.Map;
 import java.util.Random;
 import java.util.Set;
-import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.function.Supplier;
 
@@ -107,8 +105,6 @@
     @VisibleForTesting
     final SettingObserver mSettingObserver;
     private final List<EnabledOnKeyguardCallback> mEnabledOnKeyguardCallbacks;
-    private final ConcurrentLinkedQueue<BiometricPromptStatusListener>
-            mBiometricPromptStatusListeners;
     private final Random mRandom = new Random();
     @NonNull private final Supplier<Long> mRequestCounter;
     @NonNull private final BiometricContext mBiometricContext;
@@ -429,42 +425,6 @@
         }
     }
 
-    final class BiometricPromptStatusListener implements IBinder.DeathRecipient {
-        private final IBiometricPromptStatusListener mBiometricPromptStatusListener;
-
-        BiometricPromptStatusListener(IBiometricPromptStatusListener callback) {
-            mBiometricPromptStatusListener = callback;
-        }
-
-        void notifyBiometricPromptShowing() {
-            try {
-                mBiometricPromptStatusListener.onBiometricPromptShowing();
-            } catch (DeadObjectException e) {
-                Slog.w(TAG, "Death while invoking notifyHandleAuthenticate", e);
-                mBiometricPromptStatusListeners.remove(this);
-            } catch (RemoteException e) {
-                Slog.w(TAG, "Failed to invoke notifyHandleAuthenticate", e);
-            }
-        }
-
-        void notifyBiometricPromptIdle() {
-            try {
-                mBiometricPromptStatusListener.onBiometricPromptIdle();
-            } catch (DeadObjectException e) {
-                Slog.w(TAG, "Death while invoking notifyDialogDismissed", e);
-                mBiometricPromptStatusListeners.remove(this);
-            } catch (RemoteException e) {
-                Slog.w(TAG, "Failed to invoke notifyDialogDismissed", e);
-            }
-        }
-
-        @Override
-        public void binderDied() {
-            Slog.e(TAG, "Biometric prompt callback binder died");
-            mBiometricPromptStatusListeners.remove(this);
-        }
-    }
-
     // Receives events from individual biometric sensors.
     private IBiometricSensorReceiver createBiometricSensorReceiver(final long requestId) {
         return new IBiometricSensorReceiver.Stub() {
@@ -745,22 +705,6 @@
 
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override // Binder call
-        public void registerBiometricPromptStatusListener(IBiometricPromptStatusListener callback) {
-            super.registerBiometricPromptStatusListener_enforcePermission();
-
-            BiometricPromptStatusListener biometricPromptStatusListener =
-                    new BiometricPromptStatusListener(callback);
-            mBiometricPromptStatusListeners.add(biometricPromptStatusListener);
-
-            if (mAuthSession != null) {
-                biometricPromptStatusListener.notifyBiometricPromptShowing();
-            } else {
-                biometricPromptStatusListener.notifyBiometricPromptIdle();
-            }
-        }
-
-        @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
-        @Override // Binder call
         public void invalidateAuthenticatorIds(int userId, int fromSensorId,
                 IInvalidationCallback callback) {
 
@@ -1100,7 +1044,6 @@
         mDevicePolicyManager = mInjector.getDevicePolicyManager(context);
         mImpl = new BiometricServiceWrapper();
         mEnabledOnKeyguardCallbacks = new ArrayList<>();
-        mBiometricPromptStatusListeners = new ConcurrentLinkedQueue<>();
         mSettingObserver = mInjector.getSettingObserver(context, mHandler,
                 mEnabledOnKeyguardCallbacks);
         mRequestCounter = mInjector.getRequestGenerator();
@@ -1215,7 +1158,6 @@
             if (finished) {
                 Slog.d(TAG, "handleOnError: AuthSession finished");
                 mAuthSession = null;
-                notifyAuthSessionChanged();
             }
         } catch (RemoteException e) {
             Slog.e(TAG, "RemoteException", e);
@@ -1244,7 +1186,6 @@
 
         session.onDialogDismissed(reason, credentialAttestation);
         mAuthSession = null;
-        notifyAuthSessionChanged();
     }
 
     private void handleOnTryAgainPressed(long requestId) {
@@ -1294,7 +1235,6 @@
         final boolean finished = session.onClientDied();
         if (finished) {
             mAuthSession = null;
-            notifyAuthSessionChanged();
         }
     }
 
@@ -1409,16 +1349,6 @@
         });
     }
 
-    private void notifyAuthSessionChanged() {
-        for (BiometricPromptStatusListener listener : mBiometricPromptStatusListeners) {
-            if (mAuthSession == null) {
-                listener.notifyBiometricPromptIdle();
-            } else {
-                listener.notifyBiometricPromptShowing();
-            }
-        }
-    }
-
     /**
      * handleAuthenticate() (above) which is called from BiometricPrompt determines which
      * modality/modalities to start authenticating with. authenticateInternal() should only be
@@ -1456,7 +1386,6 @@
         } catch (RemoteException e) {
             Slog.e(TAG, "RemoteException", e);
         }
-        notifyAuthSessionChanged();
     }
 
     private void handleCancelAuthentication(long requestId) {
@@ -1471,7 +1400,6 @@
         if (finished) {
             Slog.d(TAG, "handleCancelAuthentication: AuthSession finished");
             mAuthSession = null;
-            notifyAuthSessionChanged();
         }
     }
 
diff --git a/services/core/java/com/android/server/compat/overrides/OWNERS b/services/core/java/com/android/server/compat/overrides/OWNERS
index b80f340..6ca7803 100644
--- a/services/core/java/com/android/server/compat/overrides/OWNERS
+++ b/services/core/java/com/android/server/compat/overrides/OWNERS
@@ -1,2 +1,2 @@
-tomnatan@google.com
+mcarli@google.com
 mariiasand@google.com
diff --git a/services/core/java/com/android/server/content/SyncOperation.java b/services/core/java/com/android/server/content/SyncOperation.java
index 64b17e5..84be521 100644
--- a/services/core/java/com/android/server/content/SyncOperation.java
+++ b/services/core/java/com/android/server/content/SyncOperation.java
@@ -588,8 +588,7 @@
             return wakeLockName;
         }
         return (wakeLockName = target.provider
-                + "/" + target.account.type
-                + "/" + target.account.name);
+                + "/" + target.account.type);
     }
 
     // TODO: Test this to make sure that casting to object doesn't lose the type info for EventLog.
diff --git a/services/core/java/com/android/server/display/AmbientBrightnessStatsTracker.java b/services/core/java/com/android/server/display/AmbientBrightnessStatsTracker.java
index 3581981..58a654a 100644
--- a/services/core/java/com/android/server/display/AmbientBrightnessStatsTracker.java
+++ b/services/core/java/com/android/server/display/AmbientBrightnessStatsTracker.java
@@ -28,6 +28,7 @@
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
+import com.android.server.display.utils.DebugUtils;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -50,7 +51,10 @@
 public class AmbientBrightnessStatsTracker {
 
     private static final String TAG = "AmbientBrightnessStatsTracker";
-    private static final boolean DEBUG = false;
+
+    // To enable these logs, run:
+    // 'adb shell setprop persist.log.tag.AmbientBrightnessStatsTracker DEBUG && adb reboot'
+    private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
 
     @VisibleForTesting
     static final float[] BUCKET_BOUNDARIES_FOR_NEW_STATS =
diff --git a/services/core/java/com/android/server/display/BrightnessThrottler.java b/services/core/java/com/android/server/display/BrightnessThrottler.java
index 59844e1..bba5ba3 100644
--- a/services/core/java/com/android/server/display/BrightnessThrottler.java
+++ b/services/core/java/com/android/server/display/BrightnessThrottler.java
@@ -38,6 +38,7 @@
 import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData;
 import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel;
 import com.android.server.display.feature.DeviceConfigParameterProvider;
+import com.android.server.display.utils.DebugUtils;
 import com.android.server.display.utils.DeviceConfigParsingUtils;
 
 import java.io.PrintWriter;
@@ -58,8 +59,10 @@
 @Deprecated
 class BrightnessThrottler {
     private static final String TAG = "BrightnessThrottler";
-    private static final boolean DEBUG = false;
 
+    // To enable these logs, run:
+    // 'adb shell setprop persist.log.tag.BrightnessThrottler DEBUG && adb reboot'
+    private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
     private static final int THROTTLING_INVALID = -1;
 
     private final Injector mInjector;
diff --git a/services/core/java/com/android/server/display/BrightnessTracker.java b/services/core/java/com/android/server/display/BrightnessTracker.java
index d550650..ac5dd20 100644
--- a/services/core/java/com/android/server/display/BrightnessTracker.java
+++ b/services/core/java/com/android/server/display/BrightnessTracker.java
@@ -64,6 +64,7 @@
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
+import com.android.server.display.utils.DebugUtils;
 
 import libcore.io.IoUtils;
 
@@ -91,8 +92,10 @@
 public class BrightnessTracker {
 
     static final String TAG = "BrightnessTracker";
-    static final boolean DEBUG = false;
 
+    // To enable these logs, run:
+    // 'adb shell setprop persist.log.tag.BrightnessTracker DEBUG && adb reboot'
+    static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
     private static final String EVENTS_FILE = "brightness_events.xml";
     private static final String AMBIENT_BRIGHTNESS_STATS_FILE = "ambient_brightness_stats.xml";
     private static final int MAX_EVENTS = 100;
diff --git a/services/core/java/com/android/server/display/ColorFade.java b/services/core/java/com/android/server/display/ColorFade.java
index 0d6635d..3de188f 100644
--- a/services/core/java/com/android/server/display/ColorFade.java
+++ b/services/core/java/com/android/server/display/ColorFade.java
@@ -42,6 +42,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.LocalServices;
+import com.android.server.display.utils.DebugUtils;
 import com.android.server.policy.WindowManagerPolicy;
 
 import libcore.io.Streams;
@@ -66,7 +67,9 @@
 final class ColorFade {
     private static final String TAG = "ColorFade";
 
-    private static final boolean DEBUG = false;
+    // To enable these logs, run:
+    // 'adb shell setprop persist.log.tag.ColorFade DEBUG && adb reboot'
+    private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
 
     // The layer for the electron beam surface.
     // This is currently hardcoded to be one layer above the boot animation.
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index cfbe0c6..a0beedb 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -77,6 +77,7 @@
 import com.android.server.display.config.ThresholdPoint;
 import com.android.server.display.config.UsiVersion;
 import com.android.server.display.config.XmlParser;
+import com.android.server.display.utils.DebugUtils;
 
 import org.xmlpull.v1.XmlPullParserException;
 
@@ -519,7 +520,10 @@
  */
 public class DisplayDeviceConfig {
     private static final String TAG = "DisplayDeviceConfig";
-    private static final boolean DEBUG = false;
+
+    // To enable these logs, run:
+    // 'adb shell setprop persist.log.tag.DisplayDeviceConfig DEBUG && adb reboot'
+    private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
 
     public static final float HIGH_BRIGHTNESS_MODE_UNSUPPORTED = Float.NaN;
 
diff --git a/services/core/java/com/android/server/display/DisplayDeviceRepository.java b/services/core/java/com/android/server/display/DisplayDeviceRepository.java
index ea52a3d..67e612d 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceRepository.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceRepository.java
@@ -24,6 +24,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.server.display.DisplayManagerService.SyncRoot;
+import com.android.server.display.utils.DebugUtils;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -39,7 +40,10 @@
  */
 class DisplayDeviceRepository implements DisplayAdapter.Listener {
     private static final String TAG = "DisplayDeviceRepository";
-    private static final Boolean DEBUG = false;
+
+    // To enable these logs, run:
+    // 'adb shell setprop persist.log.tag.DisplayDeviceRepository DEBUG && adb reboot'
+    private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
 
     public static final int DISPLAY_DEVICE_EVENT_ADDED = 1;
     public static final int DISPLAY_DEVICE_EVENT_REMOVED = 3;
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 57b2c24..087cf20 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -161,6 +161,7 @@
 import com.android.server.display.layout.Layout;
 import com.android.server.display.mode.DisplayModeDirector;
 import com.android.server.display.notifications.DisplayNotificationManager;
+import com.android.server.display.utils.DebugUtils;
 import com.android.server.display.utils.SensorUtils;
 import com.android.server.input.InputManagerInternal;
 import com.android.server.utils.FoldSettingProvider;
@@ -225,7 +226,10 @@
  */
 public final class DisplayManagerService extends SystemService {
     private static final String TAG = "DisplayManagerService";
-    private static final boolean DEBUG = false;
+
+    // To enable these logs, run:
+    // 'adb shell setprop persist.log.tag.DisplayManagerService DEBUG && adb reboot'
+    private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
 
     // When this system property is set to 0, WFD is forcibly disabled on boot.
     // When this system property is set to 1, WFD is forcibly enabled on boot.
@@ -586,8 +590,7 @@
         mSystemReady = false;
         mConfigParameterProvider = new DeviceConfigParameterProvider(DeviceConfigInterface.REAL);
         mExtraDisplayLoggingPackageName = DisplayProperties.debug_vri_package().orElse(null);
-        // TODO: b/306170135 - return TextUtils package name check instead
-        mExtraDisplayEventLogging = true;
+        mExtraDisplayEventLogging = !TextUtils.isEmpty(mExtraDisplayLoggingPackageName);
     }
 
     public void setupSchedulerPolicies() {
@@ -3044,8 +3047,7 @@
     }
 
     private boolean extraLogging(String packageName) {
-        // TODO: b/306170135 - return mExtraDisplayLoggingPackageName & package name check instead
-        return true;
+        return mExtraDisplayEventLogging && mExtraDisplayLoggingPackageName.equals(packageName);
     }
 
     // Runs on Handler thread.
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index ce98559..915f5db 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -79,6 +79,7 @@
 import com.android.server.display.color.ColorDisplayService.ReduceBrightColorsListener;
 import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.layout.Layout;
+import com.android.server.display.utils.DebugUtils;
 import com.android.server.display.utils.SensorUtils;
 import com.android.server.display.whitebalance.DisplayWhiteBalanceController;
 import com.android.server.display.whitebalance.DisplayWhiteBalanceFactory;
@@ -115,7 +116,11 @@
     private static final String SCREEN_ON_BLOCKED_TRACE_NAME = "Screen on blocked";
     private static final String SCREEN_OFF_BLOCKED_TRACE_NAME = "Screen off blocked";
 
-    private static final boolean DEBUG = false;
+    private static final String TAG = "DisplayPowerController";
+    // To enable these logs, run:
+    // 'adb shell setprop persist.log.tag.DisplayPowerController DEBUG && adb reboot'
+    private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
+
     private static final boolean DEBUG_PRETEND_PROXIMITY_SENSOR_ABSENT = false;
 
     // If true, uses the color fade on animation.
@@ -608,7 +613,7 @@
         mClock = mInjector.getClock();
         mLogicalDisplay = logicalDisplay;
         mDisplayId = mLogicalDisplay.getDisplayIdLocked();
-        mTag = "DisplayPowerController[" + mDisplayId + "]";
+        mTag = TAG + "[" + mDisplayId + "]";
         mHighBrightnessModeMetadata = hbmMetadata;
         mSuspendBlockerIdUnfinishedBusiness = getSuspendBlockerUnfinishedBusinessId(mDisplayId);
         mSuspendBlockerIdOnStateChanged = getSuspendBlockerOnStateChangedId(mDisplayId);
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index 0f00027..fc596dc 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -82,6 +82,7 @@
 import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.layout.Layout;
 import com.android.server.display.state.DisplayStateController;
+import com.android.server.display.utils.DebugUtils;
 import com.android.server.display.utils.SensorUtils;
 import com.android.server.display.whitebalance.DisplayWhiteBalanceController;
 import com.android.server.display.whitebalance.DisplayWhiteBalanceFactory;
@@ -118,8 +119,10 @@
     private static final String SCREEN_ON_BLOCKED_TRACE_NAME = "Screen on blocked";
     private static final String SCREEN_OFF_BLOCKED_TRACE_NAME = "Screen off blocked";
 
-    private static final boolean DEBUG = false;
-
+    private static final String TAG = "DisplayPowerController2";
+    // To enable these logs, run:
+    // 'adb shell setprop persist.log.tag.DisplayPowerController2 DEBUG && adb reboot'
+    private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
 
     // If true, uses the color fade on animation.
     // We might want to turn this off if we cannot get a guarantee that the screen
@@ -503,7 +506,7 @@
                 mWakelockController, mDisplayDeviceConfig, mHandler.getLooper(),
                 () -> updatePowerState(), mDisplayId, mSensorManager);
         mDisplayStateController = new DisplayStateController(mDisplayPowerProximityStateController);
-        mTag = "DisplayPowerController2[" + mDisplayId + "]";
+        mTag = TAG + "[" + mDisplayId + "]";
         mThermalBrightnessThrottlingDataId =
                 logicalDisplay.getDisplayInfoLocked().thermalBrightnessThrottlingDataId;
         mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
diff --git a/services/core/java/com/android/server/display/DisplayPowerState.java b/services/core/java/com/android/server/display/DisplayPowerState.java
index 2c257a1..be03a80 100644
--- a/services/core/java/com/android/server/display/DisplayPowerState.java
+++ b/services/core/java/com/android/server/display/DisplayPowerState.java
@@ -26,6 +26,8 @@
 import android.view.Choreographer;
 import android.view.Display;
 
+import com.android.server.display.utils.DebugUtils;
+
 import java.io.PrintWriter;
 
 /**
@@ -48,7 +50,9 @@
 final class DisplayPowerState {
     private static final String TAG = "DisplayPowerState";
 
-    private static final boolean DEBUG = false;
+    // To enable these logs, run:
+    // 'adb shell setprop persist.log.tag.DisplayPowerState DEBUG && adb reboot'
+    private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
     private static String COUNTER_COLOR_FADE = "ColorFadeLevel";
 
     private final Handler mHandler;
diff --git a/services/core/java/com/android/server/display/HighBrightnessModeController.java b/services/core/java/com/android/server/display/HighBrightnessModeController.java
index 39172b8..a9f78fd 100644
--- a/services/core/java/com/android/server/display/HighBrightnessModeController.java
+++ b/services/core/java/com/android/server/display/HighBrightnessModeController.java
@@ -38,6 +38,7 @@
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.display.DisplayDeviceConfig.HighBrightnessModeData;
 import com.android.server.display.DisplayManagerService.Clock;
+import com.android.server.display.utils.DebugUtils;
 
 import java.io.PrintWriter;
 import java.util.ArrayDeque;
@@ -54,7 +55,9 @@
 class HighBrightnessModeController {
     private static final String TAG = "HighBrightnessModeController";
 
-    private static final boolean DEBUG = false;
+    // To enable these logs, run:
+    // 'adb shell setprop persist.log.tag.HighBrightnessModeController DEBUG && adb reboot'
+    private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
 
     @VisibleForTesting
     static final float HBM_TRANSITION_POINT_INVALID = Float.POSITIVE_INFINITY;
diff --git a/services/core/java/com/android/server/display/HysteresisLevels.java b/services/core/java/com/android/server/display/HysteresisLevels.java
index 3c522e7..0521b8a 100644
--- a/services/core/java/com/android/server/display/HysteresisLevels.java
+++ b/services/core/java/com/android/server/display/HysteresisLevels.java
@@ -18,6 +18,8 @@
 
 import android.util.Slog;
 
+import com.android.server.display.utils.DebugUtils;
+
 import java.io.PrintWriter;
 import java.util.Arrays;
 
@@ -27,7 +29,9 @@
 public class HysteresisLevels {
     private static final String TAG = "HysteresisLevels";
 
-    private static final boolean DEBUG = false;
+    // To enable these logs, run:
+    // 'adb shell setprop persist.log.tag.HysteresisLevels DEBUG && adb reboot'
+    private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
 
     private final float[] mBrighteningThresholdsPercentages;
     private final float[] mDarkeningThresholdsPercentages;
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index c55bc62..f3425d2e 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -43,6 +43,7 @@
 import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.layout.DisplayIdProducer;
 import com.android.server.display.layout.Layout;
+import com.android.server.display.utils.DebugUtils;
 import com.android.server.utils.FoldSettingProvider;
 
 import java.io.PrintWriter;
@@ -63,7 +64,9 @@
 class LogicalDisplayMapper implements DisplayDeviceRepository.Listener {
     private static final String TAG = "LogicalDisplayMapper";
 
-    private static final boolean DEBUG = false;
+    // To enable these logs, run:
+    // 'adb shell setprop persist.log.tag.LogicalDisplayMapper DEBUG && adb reboot'
+    private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
 
     public static final int LOGICAL_DISPLAY_EVENT_ADDED = 1;
     public static final int LOGICAL_DISPLAY_EVENT_CHANGED = 2;
diff --git a/services/core/java/com/android/server/display/OverlayDisplayWindow.java b/services/core/java/com/android/server/display/OverlayDisplayWindow.java
index cd3a453..3fd58e8 100644
--- a/services/core/java/com/android/server/display/OverlayDisplayWindow.java
+++ b/services/core/java/com/android/server/display/OverlayDisplayWindow.java
@@ -35,6 +35,7 @@
 import android.widget.TextView;
 
 import com.android.internal.util.DumpUtils;
+import com.android.server.display.utils.DebugUtils;
 
 import java.io.PrintWriter;
 
@@ -47,8 +48,10 @@
  */
 final class OverlayDisplayWindow implements DumpUtils.Dump {
     private static final String TAG = "OverlayDisplayWindow";
-    private static final boolean DEBUG = false;
 
+    // To enable these logs, run:
+    // 'adb shell setprop persist.log.tag.OverlayDisplayWindow DEBUG && adb reboot'
+    private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
     private final float INITIAL_SCALE = 0.5f;
     private final float MIN_SCALE = 0.3f;
     private final float MAX_SCALE = 1.0f;
diff --git a/services/core/java/com/android/server/display/WakelockController.java b/services/core/java/com/android/server/display/WakelockController.java
index 1e13974..7bc7971 100644
--- a/services/core/java/com/android/server/display/WakelockController.java
+++ b/services/core/java/com/android/server/display/WakelockController.java
@@ -21,6 +21,7 @@
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.display.utils.DebugUtils;
 
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
@@ -41,7 +42,11 @@
     @VisibleForTesting
     static final int WAKE_LOCK_MAX = WAKE_LOCK_UNFINISHED_BUSINESS;
 
-    private static final boolean DEBUG = false;
+    private static final String TAG = "WakelockController";
+
+    // To enable these logs, run:
+    // 'adb shell setprop persist.log.tag.WakelockController DEBUG && adb reboot'
+    private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
 
     @IntDef(flag = true, prefix = "WAKE_LOCK_", value = {
             WAKE_LOCK_PROXIMITY_POSITIVE,
@@ -100,7 +105,7 @@
     public WakelockController(int displayId,
             DisplayManagerInternal.DisplayPowerCallbacks callbacks) {
         mDisplayId = displayId;
-        mTag = "WakelockController[" + mDisplayId + "]";
+        mTag = TAG + "[" + mDisplayId + "]";
         mDisplayPowerCallbacks = callbacks;
         mSuspendBlockerIdUnfinishedBusiness = "[" + displayId + "]unfinished business";
         mSuspendBlockerIdOnStateChanged = "[" + displayId + "]on state changed";
diff --git a/services/core/java/com/android/server/display/WifiDisplayAdapter.java b/services/core/java/com/android/server/display/WifiDisplayAdapter.java
index e3d38e7..7660cf8 100644
--- a/services/core/java/com/android/server/display/WifiDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/WifiDisplayAdapter.java
@@ -41,6 +41,7 @@
 
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.display.utils.DebugUtils;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -65,7 +66,9 @@
 final class WifiDisplayAdapter extends DisplayAdapter {
     private static final String TAG = "WifiDisplayAdapter";
 
-    private static final boolean DEBUG = false;
+    // To enable these logs, run:
+    // 'adb shell setprop persist.log.tag.WifiDisplayAdapter DEBUG && adb reboot'
+    private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
 
     private static final int MSG_SEND_STATUS_CHANGE_BROADCAST = 1;
 
diff --git a/services/core/java/com/android/server/display/WifiDisplayController.java b/services/core/java/com/android/server/display/WifiDisplayController.java
index 955b8d9..873598a 100644
--- a/services/core/java/com/android/server/display/WifiDisplayController.java
+++ b/services/core/java/com/android/server/display/WifiDisplayController.java
@@ -45,6 +45,7 @@
 import android.view.Surface;
 
 import com.android.internal.util.DumpUtils;
+import com.android.server.display.utils.DebugUtils;
 
 import java.io.PrintWriter;
 import java.net.Inet4Address;
@@ -69,7 +70,10 @@
  */
 final class WifiDisplayController implements DumpUtils.Dump {
     private static final String TAG = "WifiDisplayController";
-    private static final boolean DEBUG = false;
+
+    // To enable these logs, run:
+    // 'adb shell setprop persist.log.tag.WifiDisplayController DEBUG && adb reboot'
+    private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
 
     private static final int DEFAULT_CONTROL_PORT = 7236;
     private static final int MAX_THROUGHPUT = 50;
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 d953e8e..7f3ea6a 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -21,6 +21,7 @@
 import android.util.Slog;
 
 import com.android.server.display.feature.flags.Flags;
+import com.android.server.display.utils.DebugUtils;
 
 import java.util.function.Supplier;
 
@@ -28,9 +29,13 @@
  * Utility class to read the flags used in the display manager server.
  */
 public class DisplayManagerFlags {
-    private static final boolean DEBUG = false;
     private static final String TAG = "DisplayManagerFlags";
 
+    // To enable these logs, run:
+    // 'adb shell setprop persist.log.tag.DisplayManagerFlags DEBUG && adb reboot'
+    private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
+
+
     private final FlagState mConnectedDisplayManagementFlagState = new FlagState(
             Flags.FLAG_ENABLE_CONNECTED_DISPLAY_MANAGEMENT,
             Flags::enableConnectedDisplayManagement);
diff --git a/services/core/java/com/android/server/display/utils/DebugUtils.java b/services/core/java/com/android/server/display/utils/DebugUtils.java
new file mode 100644
index 0000000..1496495
--- /dev/null
+++ b/services/core/java/com/android/server/display/utils/DebugUtils.java
@@ -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.server.display.utils;
+
+import android.util.Log;
+
+public class DebugUtils {
+
+    public static final boolean DEBUG_ALL = Log.isLoggable("DisplayManager_All", Log.DEBUG);
+
+    /**
+     * Returns whether the specified tag has logging enabled. Use the tag name specified in the
+     * calling class, or DisplayManager_All to globally enable all tags in display.
+     * To enable:
+     * adb shell setprop persist.log.tag.DisplayManager_All DEBUG
+     * To disable:
+     * adb shell setprop persist.log.tag.DisplayManager_All \"\"
+     */
+    public static boolean isDebuggable(String tag) {
+        return Log.isLoggable(tag, Log.DEBUG) || DEBUG_ALL;
+    }
+}
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index f35b045..568618e 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -697,9 +697,9 @@
             return;
         }
 
-        if (isUserKeyUnlocked(userId)) {
-            // If storage is not locked, the user will be automatically unlocked so there is
-            // no need to show the notification.
+        if (isCeStorageUnlocked(userId)) {
+            // If the user's CE storage is already unlocked, then the user will be automatically
+            // unlocked, so there is no need to show the notification.
             return;
         }
 
@@ -1030,8 +1030,8 @@
             // they did have an SP then their CE key wasn't encrypted by it.
             //
             // If this gets interrupted (e.g. by the device powering off), there shouldn't be a
-            // problem since this will run again on the next boot, and setUserKeyProtection() is
-            // okay with the key being already protected by the given secret.
+            // problem since this will run again on the next boot, and setCeStorageProtection() is
+            // okay with the CE key being already protected by the given secret.
             if (getString(MIGRATED_SP_CE_ONLY, null, 0) == null) {
                 for (UserInfo user : mUserManager.getAliveUsers()) {
                     removeStateForReusedUserIdIfNecessary(user.id, user.serialNumber);
@@ -1066,7 +1066,7 @@
                 Slogf.wtf(TAG, "Failed to unwrap synthetic password for unsecured user %d", userId);
                 return;
             }
-            setUserKeyProtection(userId, result.syntheticPassword);
+            setCeStorageProtection(userId, result.syntheticPassword);
         }
     }
 
@@ -2005,11 +2005,11 @@
         mStorage.writeChildProfileLock(profileUserId, ArrayUtils.concat(iv, ciphertext));
     }
 
-    private void setUserKeyProtection(@UserIdInt int userId, SyntheticPassword sp) {
+    private void setCeStorageProtection(@UserIdInt int userId, SyntheticPassword sp) {
         final byte[] secret = sp.deriveFileBasedEncryptionKey();
         final long callingId = Binder.clearCallingIdentity();
         try {
-            mStorageManager.setUserKeyProtection(userId, secret);
+            mStorageManager.setCeStorageProtection(userId, secret);
         } catch (RemoteException e) {
             throw new IllegalStateException("Failed to protect CE key for user " + userId, e);
         } finally {
@@ -2017,11 +2017,11 @@
         }
     }
 
-    private boolean isUserKeyUnlocked(int userId) {
+    private boolean isCeStorageUnlocked(int userId) {
         try {
-            return mStorageManager.isUserKeyUnlocked(userId);
+            return mStorageManager.isCeStorageUnlocked(userId);
         } catch (RemoteException e) {
-            Slog.e(TAG, "failed to check user key locked state", e);
+            Slog.e(TAG, "Error checking whether CE storage is unlocked", e);
             return false;
         }
     }
@@ -2032,8 +2032,8 @@
      * This method doesn't throw exceptions because it is called opportunistically whenever a user
      * is started.  Whether it worked or not can be detected by whether the key got unlocked or not.
      */
-    private void unlockUserKey(@UserIdInt int userId, SyntheticPassword sp) {
-        if (isUserKeyUnlocked(userId)) {
+    private void unlockCeStorage(@UserIdInt int userId, SyntheticPassword sp) {
+        if (isCeStorageUnlocked(userId)) {
             Slogf.d(TAG, "CE storage for user %d is already unlocked", userId);
             return;
         }
@@ -2041,7 +2041,7 @@
         final String userType = isUserSecure(userId) ? "secured" : "unsecured";
         final byte[] secret = sp.deriveFileBasedEncryptionKey();
         try {
-            mStorageManager.unlockUserKey(userId, userInfo.serialNumber, secret);
+            mStorageManager.unlockCeStorage(userId, userInfo.serialNumber, secret);
             Slogf.i(TAG, "Unlocked CE storage for %s user %d", userType, userId);
         } catch (RemoteException e) {
             Slogf.wtf(TAG, e, "Failed to unlock CE storage for %s user %d", userType, userId);
@@ -2054,8 +2054,10 @@
     public void unlockUserKeyIfUnsecured(@UserIdInt int userId) {
         checkPasswordReadPermission();
         synchronized (mSpManager) {
-            if (isUserKeyUnlocked(userId)) {
+            if (isCeStorageUnlocked(userId)) {
                 Slogf.d(TAG, "CE storage for user %d is already unlocked", userId);
+                // This method actually does more than unlock CE storage.  However, if CE storage is
+                // already unlocked, then the other parts must have already been done too.
                 return;
             }
             if (isUserSecure(userId)) {
@@ -2072,7 +2074,7 @@
                 return;
             }
             onSyntheticPasswordUnlocked(userId, result.syntheticPassword);
-            unlockUserKey(userId, result.syntheticPassword);
+            unlockCeStorage(userId, result.syntheticPassword);
         }
     }
 
@@ -2775,7 +2777,7 @@
             final long protectorId = mSpManager.createLskfBasedProtector(getGateKeeperService(),
                     LockscreenCredential.createNone(), sp, userId);
             setCurrentLskfBasedProtectorId(protectorId, userId);
-            setUserKeyProtection(userId, sp);
+            setCeStorageProtection(userId, sp);
             onSyntheticPasswordCreated(userId, sp);
             Slogf.i(TAG, "Successfully initialized synthetic password for user %d", userId);
             return sp;
@@ -2836,7 +2838,7 @@
 
         unlockKeystore(userId, sp);
 
-        unlockUserKey(userId, sp);
+        unlockCeStorage(userId, sp);
 
         unlockUser(userId);
 
@@ -2900,7 +2902,7 @@
 
             mSpManager.clearSidForUser(userId);
             gateKeeperClearSecureUserId(userId);
-            unlockUserKey(userId, sp);
+            unlockCeStorage(userId, sp);
             unlockKeystore(userId, sp);
             setKeystorePassword(null, userId);
             removeBiometricsForUser(userId);
diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
index 67a1ccd..78077a8 100644
--- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
+++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
@@ -40,6 +40,7 @@
 import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.media.flags.Flags;
 
 import java.util.List;
 import java.util.Objects;
@@ -358,21 +359,8 @@
 
             RoutingSessionInfo newSessionInfo = builder.setProviderId(mUniqueId).build();
 
-            if (mPendingSessionCreationRequest != null) {
-                SessionCreationRequest sessionCreationRequest;
-                synchronized (mRequestLock) {
-                    sessionCreationRequest = mPendingSessionCreationRequest;
-                    mPendingSessionCreationRequest = null;
-                }
-                if (sessionCreationRequest != null) {
-                    if (TextUtils.equals(mSelectedRouteId, sessionCreationRequest.mRouteId)) {
-                        mCallback.onSessionCreated(this,
-                                sessionCreationRequest.mRequestId, newSessionInfo);
-                    } else {
-                        mCallback.onRequestFailed(this, sessionCreationRequest.mRequestId,
-                                MediaRoute2ProviderService.REASON_UNKNOWN_ERROR);
-                    }
-                }
+            synchronized (mRequestLock) {
+                reportPendingSessionRequestResultLockedIfNeeded(newSessionInfo);
             }
 
             if (Objects.equals(oldSessionInfo, newSessionInfo)) {
@@ -395,6 +383,59 @@
         }
     }
 
+    @GuardedBy("mRequestLock")
+    private void reportPendingSessionRequestResultLockedIfNeeded(
+            RoutingSessionInfo newSessionInfo) {
+        if (mPendingSessionCreationRequest == null) {
+            // No pending request, nothing to report.
+            return;
+        }
+
+        long pendingRequestId = mPendingSessionCreationRequest.mRequestId;
+        if (TextUtils.equals(mSelectedRouteId, mPendingSessionCreationRequest.mRouteId)) {
+            if (DEBUG) {
+                Slog.w(
+                        TAG,
+                        "Session creation success to route "
+                                + mPendingSessionCreationRequest.mRouteId);
+            }
+            mPendingSessionCreationRequest = null;
+            mCallback.onSessionCreated(this, pendingRequestId, newSessionInfo);
+        } else {
+            boolean isRequestedRouteConnectedBtRoute = isRequestedRouteConnectedBtRoute();
+            if (!Flags.enableWaitingStateForSystemSessionCreationRequest()
+                    || !isRequestedRouteConnectedBtRoute) {
+                if (DEBUG) {
+                    Slog.w(
+                            TAG,
+                            "Session creation failed to route "
+                                    + mPendingSessionCreationRequest.mRouteId);
+                }
+                mPendingSessionCreationRequest = null;
+                mCallback.onRequestFailed(
+                        this, pendingRequestId, MediaRoute2ProviderService.REASON_UNKNOWN_ERROR);
+            } else if (DEBUG) {
+                Slog.w(
+                        TAG,
+                        "Session creation waiting state to route "
+                                + mPendingSessionCreationRequest.mRouteId);
+            }
+        }
+    }
+
+    @GuardedBy("mRequestLock")
+    private boolean isRequestedRouteConnectedBtRoute() {
+        // Using AllRoutes instead of TransferableRoutes as BT Stack sends an intermediate update
+        // where two BT routes are active so the transferable routes list is empty.
+        // See b/307723189 for context
+        for (MediaRoute2Info btRoute : mBluetoothRouteController.getAllBluetoothRoutes()) {
+            if (TextUtils.equals(btRoute.getId(), mPendingSessionCreationRequest.mRouteId)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     void publishProviderState() {
         updateProviderState();
         notifyProviderState();
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index 58927d1..893ed61 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -470,6 +470,11 @@
     }
 
     @VisibleForTesting
+    void notifyPermissionRequestCancelled(int hostUid) {
+        mMediaProjectionMetricsLogger.logProjectionPermissionRequestCancelled(hostUid);
+    }
+
+    @VisibleForTesting
     void notifyAppSelectorDisplayed(int hostUid) {
         mMediaProjectionMetricsLogger.logAppSelectorDisplayed(hostUid);
     }
@@ -852,19 +857,6 @@
 
         @Override // Binder call
         @EnforcePermission(MANAGE_MEDIA_PROJECTION)
-        public void notifyPermissionRequestStateChange(int hostUid, int state,
-                int sessionCreationSource) {
-            notifyPermissionRequestStateChange_enforcePermission();
-            final long token = Binder.clearCallingIdentity();
-            try {
-                mMediaProjectionMetricsLogger.notifyProjectionStateChange(hostUid, state, sessionCreationSource);
-            } finally {
-                Binder.restoreCallingIdentity(token);
-            }
-        }
-
-        @Override // Binder call
-        @EnforcePermission(MANAGE_MEDIA_PROJECTION)
         public void notifyPermissionRequestInitiated(int hostUid, int sessionCreationSource) {
             notifyPermissionRequestInitiated_enforcePermission();
             final long token = Binder.clearCallingIdentity();
@@ -890,6 +882,18 @@
 
         @Override // Binder call
         @EnforcePermission(MANAGE_MEDIA_PROJECTION)
+        public void notifyPermissionRequestCancelled(int hostUid) {
+            notifyPermissionRequestCancelled_enforcePermission();
+            final long token = Binder.clearCallingIdentity();
+            try {
+                MediaProjectionManagerService.this.notifyPermissionRequestCancelled(hostUid);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        @Override // Binder call
+        @EnforcePermission(MANAGE_MEDIA_PROJECTION)
         public void notifyAppSelectorDisplayed(int hostUid) {
             notifyAppSelectorDisplayed_enforcePermission();
             final long token = Binder.clearCallingIdentity();
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionMetricsLogger.java b/services/core/java/com/android/server/media/projection/MediaProjectionMetricsLogger.java
index 55a30bf..d7fefeb 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionMetricsLogger.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionMetricsLogger.java
@@ -118,6 +118,23 @@
     }
 
     /**
+     * Logs that requesting permission for media projection was cancelled by the user.
+     *
+     * @param hostUid UID of the package that initiates MediaProjection.
+     */
+    public void logProjectionPermissionRequestCancelled(int hostUid) {
+        write(
+                mSessionIdGenerator.getCurrentSessionId(),
+                FrameworkStatsLog
+                        .MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_CANCELLED,
+                hostUid,
+                TARGET_UID_UNKNOWN,
+                TIME_SINCE_LAST_ACTIVE_UNKNOWN,
+                FrameworkStatsLog
+                        .MEDIA_PROJECTION_STATE_CHANGED__CREATION_SOURCE__CREATION_SOURCE_UNKNOWN);
+    }
+
+    /**
      * Logs that the app selector dialog is shown for the user.
      *
      * @param hostUid UID of the package that initiates MediaProjection.
@@ -174,23 +191,6 @@
         }
     }
 
-    public void notifyProjectionStateChange(int hostUid, int state, int sessionCreationSource) {
-        write(hostUid, state, sessionCreationSource);
-    }
-
-    private void write(int hostUid, int state, int sessionCreationSource) {
-        mFrameworkStatsLogWrapper.write(
-                /* code */ FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED,
-                /* session_id */ 123,
-                /* state */ state,
-                /* previous_state */ FrameworkStatsLog
-                        .MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_UNKNOWN,
-                /* host_uid */ hostUid,
-                /* target_uid */ -1,
-                /* time_since_last_active */ 0,
-                /* creation_source */ sessionCreationSource);
-    }
-
     private void write(
             int sessionId,
             int state,
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 7ca5699..4b5d52f 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -244,6 +244,7 @@
 import android.permission.PermissionManager;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
+import android.provider.Settings.Secure;
 import android.service.notification.Adjustment;
 import android.service.notification.Condition;
 import android.service.notification.ConversationChannelWrapper;
@@ -2008,6 +2009,8 @@
                         Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
         private final Uri LOCK_SCREEN_SHOW_NOTIFICATIONS
                 = Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS);
+        private final Uri SHOW_NOTIFICATION_SNOOZE
+                = Settings.Secure.getUriFor(Settings.Secure.SHOW_NOTIFICATION_SNOOZE);
 
         SettingsObserver(Handler handler) {
             super(handler);
@@ -2034,6 +2037,10 @@
                     false, this, UserHandle.USER_ALL);
             resolver.registerContentObserver(LOCK_SCREEN_SHOW_NOTIFICATIONS,
                     false, this, UserHandle.USER_ALL);
+
+            resolver.registerContentObserver(SHOW_NOTIFICATION_SNOOZE,
+                    false, this, UserHandle.USER_ALL);
+
             update(null);
         }
 
@@ -2083,6 +2090,14 @@
             if (uri == null || LOCK_SCREEN_SHOW_NOTIFICATIONS.equals(uri)) {
                 mPreferencesHelper.updateLockScreenShowNotifications();
             }
+            if (SHOW_NOTIFICATION_SNOOZE.equals(uri)) {
+                final boolean snoozeEnabled = Settings.Secure.getIntForUser(resolver,
+                        Secure.SHOW_NOTIFICATION_SNOOZE, 0, UserHandle.USER_CURRENT)
+                        != 0;
+                if (!snoozeEnabled) {
+                    unsnoozeAll();
+                }
+            }
         }
     }
 
@@ -7792,6 +7807,13 @@
         }
     }
 
+    private void unsnoozeAll() {
+        synchronized (mNotificationLock) {
+            mSnoozeHelper.repostAll(mUserProfiles.getCurrentProfileIds());
+            handleSavePolicyFile();
+        }
+    }
+
     protected class CancelNotificationRunnable implements Runnable {
         private final int mCallingUid;
         private final int mCallingPid;
diff --git a/services/core/java/com/android/server/notification/OWNERS b/services/core/java/com/android/server/notification/OWNERS
index 72c4529..9f16662 100644
--- a/services/core/java/com/android/server/notification/OWNERS
+++ b/services/core/java/com/android/server/notification/OWNERS
@@ -1,6 +1,9 @@
-# Bug component: 1305560
+# Bug component: 78010
 
 juliacr@google.com
 yurilin@google.com
+aroederer@google.com
+matiashe@google.com
+valiiftime@google.com
 jeffdq@google.com
 dsandler@android.com
\ No newline at end of file
diff --git a/services/core/java/com/android/server/notification/SnoozeHelper.java b/services/core/java/com/android/server/notification/SnoozeHelper.java
index 0176989..8f5676b3 100644
--- a/services/core/java/com/android/server/notification/SnoozeHelper.java
+++ b/services/core/java/com/android/server/notification/SnoozeHelper.java
@@ -296,6 +296,20 @@
         }
     }
 
+    /**
+     * Unsnooze & repost all snoozed notifications for userId and its profiles
+     */
+    protected void repostAll(IntArray userIds) {
+        synchronized (mLock) {
+            List<NotificationRecord> snoozedNotifications = getSnoozed();
+            for (NotificationRecord r : snoozedNotifications) {
+                if (userIds.binarySearch(r.getUserId()) >= 0) {
+                    repost(r.getKey(), r.getUserId(), false);
+                }
+            }
+        }
+    }
+
     protected void repost(String key, boolean muteOnReturn) {
         synchronized (mLock) {
             final NotificationRecord r = mSnoozedNotifications.get(key);
diff --git a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
index 1134714..e57ea0f 100644
--- a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
+++ b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
@@ -16,6 +16,8 @@
 
 package com.android.server.os;
 
+import static android.app.admin.flags.Flags.onboardingBugreportV2Enabled;
+
 import android.Manifest;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
@@ -52,8 +54,10 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.util.HashSet;
 import java.util.Objects;
 import java.util.OptionalInt;
+import java.util.Set;
 
 /**
  * Implementation of the service that provides a privileged API to capture and consume bugreports.
@@ -101,10 +105,12 @@
         private final ArrayMap<Pair<Integer, String>, ArraySet<String>> mBugreportFiles =
                 new ArrayMap<>();
 
+        @GuardedBy("mLock")
+        private final Set<String> mBugreportFilesToPersist = new HashSet<>();
+
         /**
          * Checks that a given file was generated on behalf of the given caller. If the file was
-         * generated on behalf of the caller, it is removed from the bugreport mapping so that it
-         * may not be retrieved again. If the file was not generated on behalf of the caller, an
+         * not generated on behalf of the caller, an
          * {@link IllegalArgumentException} is thrown.
          *
          * @param callingInfo a (uid, package name) pair identifying the caller
@@ -114,35 +120,76 @@
          * @throws IllegalArgumentException if {@code bugreportFile} is not associated with
          *                                  {@code callingInfo}.
          */
+        @RequiresPermission(value = android.Manifest.permission.INTERACT_ACROSS_USERS,
+                conditional = true)
         void ensureCallerPreviouslyGeneratedFile(
-                Pair<Integer, String> callingInfo, String bugreportFile) {
+                Context context, Pair<Integer, String> callingInfo, int userId,
+                String bugreportFile) {
             synchronized (mLock) {
-                ArraySet<String> bugreportFilesForCaller = mBugreportFiles.get(callingInfo);
-                if (bugreportFilesForCaller != null
-                        && bugreportFilesForCaller.contains(bugreportFile)) {
-                    bugreportFilesForCaller.remove(bugreportFile);
-                    if (bugreportFilesForCaller.isEmpty()) {
-                        mBugreportFiles.remove(callingInfo);
+                if (onboardingBugreportV2Enabled()) {
+                    final int uidForUser = Binder.withCleanCallingIdentity(() -> {
+                        try {
+                            return context.getPackageManager()
+                                    .getPackageUidAsUser(callingInfo.second, userId);
+                        } catch (PackageManager.NameNotFoundException exception) {
+                            throwInvalidBugreportFileForCallerException(
+                                    bugreportFile, callingInfo.second);
+                            return -1;
+                        }
+                    });
+                    if (uidForUser != callingInfo.first && context.checkCallingOrSelfPermission(
+                            Manifest.permission.INTERACT_ACROSS_USERS)
+                            != PackageManager.PERMISSION_GRANTED) {
+                        throw new SecurityException(
+                                callingInfo.second + " does not hold the "
+                                        + "INTERACT_ACROSS_USERS permission to access "
+                                        + "cross-user bugreports.");
+                    }
+                    ArraySet<String> bugreportFilesForUid = mBugreportFiles.get(
+                            new Pair<>(uidForUser, callingInfo.second));
+                    if (bugreportFilesForUid == null
+                            || !bugreportFilesForUid.contains(bugreportFile)) {
+                        throwInvalidBugreportFileForCallerException(
+                                bugreportFile, callingInfo.second);
                     }
                 } else {
-                    throw new IllegalArgumentException(
-                            "File " + bugreportFile + " was not generated"
-                                    + " on behalf of calling package " + callingInfo.second);
+                    ArraySet<String> bugreportFilesForCaller = mBugreportFiles.get(callingInfo);
+                    if (bugreportFilesForCaller != null
+                            && bugreportFilesForCaller.contains(bugreportFile)) {
+                        bugreportFilesForCaller.remove(bugreportFile);
+                        if (bugreportFilesForCaller.isEmpty()) {
+                            mBugreportFiles.remove(callingInfo);
+                        }
+                    } else {
+                        throwInvalidBugreportFileForCallerException(
+                                bugreportFile, callingInfo.second);
+
+                    }
                 }
             }
         }
 
+        private static void throwInvalidBugreportFileForCallerException(
+                String bugreportFile, String packageName) {
+            throw new IllegalArgumentException("File " + bugreportFile + " was not generated on"
+                    + " behalf of calling package " + packageName);
+        }
+
         /**
          * Associates a bugreport file with a caller, which is identified as a
          * (uid, package name) pair.
          */
-        void addBugreportFileForCaller(Pair<Integer, String> caller, String bugreportFile) {
+        void addBugreportFileForCaller(
+                Pair<Integer, String> caller, String bugreportFile, boolean keepOnRetrieval) {
             synchronized (mLock) {
                 if (!mBugreportFiles.containsKey(caller)) {
                     mBugreportFiles.put(caller, new ArraySet<>());
                 }
                 ArraySet<String> bugreportFilesForCaller = mBugreportFiles.get(caller);
                 bugreportFilesForCaller.add(bugreportFile);
+                if ((onboardingBugreportV2Enabled()) && keepOnRetrieval) {
+                    mBugreportFilesToPersist.add(bugreportFile);
+                }
             }
         }
     }
@@ -246,16 +293,17 @@
     }
 
     @Override
-    @RequiresPermission(Manifest.permission.DUMP)
-    public void retrieveBugreport(int callingUidUnused, String callingPackage,
-            FileDescriptor bugreportFd, String bugreportFile, IDumpstateListener listener) {
+    @RequiresPermission(value = Manifest.permission.DUMP, conditional = true)
+    public void retrieveBugreport(int callingUidUnused, String callingPackage, int userId,
+            FileDescriptor bugreportFd, String bugreportFile,
+            boolean keepBugreportOnRetrievalUnused, IDumpstateListener listener) {
         int callingUid = Binder.getCallingUid();
         enforcePermission(callingPackage, callingUid, false);
 
         Slogf.i(TAG, "Retrieving bugreport for %s / %d", callingPackage, callingUid);
         try {
             mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(
-                    new Pair<>(callingUid, callingPackage), bugreportFile);
+                    mContext, new Pair<>(callingUid, callingPackage), userId, bugreportFile);
         } catch (IllegalArgumentException e) {
             Slog.e(TAG, e.getMessage());
             reportError(listener, IDumpstateListener.BUGREPORT_ERROR_NO_BUGREPORT_TO_RETRIEVE);
@@ -281,10 +329,17 @@
             // Wrap the listener so we can intercept binder events directly.
             DumpstateListener myListener = new DumpstateListener(listener, ds,
                     new Pair<>(callingUid, callingPackage), /* reportFinishedFile= */ true);
+
+            boolean keepBugreportOnRetrieval = false;
+            if (onboardingBugreportV2Enabled()) {
+                keepBugreportOnRetrieval = mBugreportFileManager.mBugreportFilesToPersist.contains(
+                        bugreportFile);
+            }
+
             setCurrentDumpstateListenerLocked(myListener);
             try {
-                ds.retrieveBugreport(callingUid, callingPackage, bugreportFd,
-                        bugreportFile, myListener);
+                ds.retrieveBugreport(callingUid, callingPackage, userId, bugreportFd,
+                        bugreportFile, keepBugreportOnRetrieval, myListener);
             } catch (RemoteException e) {
                 Slog.e(TAG, "RemoteException in retrieveBugreport", e);
             }
@@ -317,7 +372,8 @@
     private void validateBugreportFlags(int flags) {
         flags = clearBugreportFlag(flags,
                 BugreportParams.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA
-                        | BugreportParams.BUGREPORT_FLAG_DEFER_CONSENT);
+                        | BugreportParams.BUGREPORT_FLAG_DEFER_CONSENT
+                        | BugreportParams.BUGREPORT_FLAG_KEEP_BUGREPORT_ON_RETRIEVAL);
         if (flags != 0) {
             Slog.w(TAG, "Unknown bugreport flags: " + flags);
             throw new IllegalArgumentException("Unknown bugreport flags: " + flags);
@@ -482,6 +538,9 @@
         boolean reportFinishedFile =
                 (bugreportFlags & BugreportParams.BUGREPORT_FLAG_DEFER_CONSENT) != 0;
 
+        boolean keepBugreportOnRetrieval =
+                (bugreportFlags & BugreportParams.BUGREPORT_FLAG_KEEP_BUGREPORT_ON_RETRIEVAL) != 0;
+
         IDumpstate ds = startAndGetDumpstateBinderServiceLocked();
         if (ds == null) {
             Slog.w(TAG, "Unable to get bugreport service");
@@ -490,7 +549,8 @@
         }
 
         DumpstateListener myListener = new DumpstateListener(listener, ds,
-                new Pair<>(callingUid, callingPackage), reportFinishedFile);
+                new Pair<>(callingUid, callingPackage), reportFinishedFile,
+                keepBugreportOnRetrieval);
         setCurrentDumpstateListenerLocked(myListener);
         try {
             ds.startBugreport(callingUid, callingPackage, bugreportFd, screenshotFd, bugreportMode,
@@ -646,9 +706,16 @@
         private final boolean mReportFinishedFile;
         private int mProgress; // used for debugging purposes only
         private boolean mDone;
+        private boolean mKeepBugreportOnRetrieval;
 
         DumpstateListener(IDumpstateListener listener, IDumpstate ds,
                 Pair<Integer, String> caller, boolean reportFinishedFile) {
+            this(listener, ds, caller, reportFinishedFile, /* keepBugreportOnRetrieval= */ false);
+        }
+
+        DumpstateListener(IDumpstateListener listener, IDumpstate ds,
+                Pair<Integer, String> caller, boolean reportFinishedFile,
+                boolean keepBugreportOnRetrieval) {
             if (DEBUG) {
                 Slogf.d(TAG, "Starting DumpstateListener(id=%d) for caller %s", mId, caller);
             }
@@ -656,6 +723,7 @@
             mDs = ds;
             mCaller = caller;
             mReportFinishedFile = reportFinishedFile;
+            mKeepBugreportOnRetrieval = keepBugreportOnRetrieval;
             try {
                 mDs.asBinder().linkToDeath(this, 0);
             } catch (RemoteException e) {
@@ -690,7 +758,8 @@
                 reportFinishedLocked("File: " + bugreportFile);
             }
             if (mReportFinishedFile) {
-                mBugreportFileManager.addBugreportFileForCaller(mCaller, bugreportFile);
+                mBugreportFileManager.addBugreportFileForCaller(
+                        mCaller, bugreportFile, mKeepBugreportOnRetrieval);
             } else if (DEBUG) {
                 Slog.d(TAG, "Not reporting finished file");
             }
diff --git a/services/core/java/com/android/server/pm/AppDataHelper.java b/services/core/java/com/android/server/pm/AppDataHelper.java
index bd9be30..167c17c 100644
--- a/services/core/java/com/android/server/pm/AppDataHelper.java
+++ b/services/core/java/com/android/server/pm/AppDataHelper.java
@@ -121,7 +121,7 @@
                 StorageManagerInternal.class);
         for (UserInfo user : umInternal.getUsers(false /*excludeDying*/)) {
             final int flags;
-            if (StorageManager.isUserKeyUnlocked(user.id)
+            if (StorageManager.isCeStorageUnlocked(user.id)
                     && smInternal.isCeStoragePrepared(user.id)) {
                 flags = StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE;
             } else if (umInternal.isUserRunning(user.id)) {
@@ -410,7 +410,7 @@
         // First look for stale data that doesn't belong, and check if things
         // have changed since we did our last restorecon
         if ((flags & StorageManager.FLAG_STORAGE_CE) != 0) {
-            if (StorageManager.isFileEncrypted() && !StorageManager.isUserKeyUnlocked(userId)) {
+            if (StorageManager.isFileEncrypted() && !StorageManager.isCeStorageUnlocked(userId)) {
                 throw new RuntimeException(
                         "Yikes, someone asked us to reconcile CE storage while " + userId
                                 + " was still locked; this would have caused massive data loss!");
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 510c06e..3ed9f02 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -3556,7 +3556,7 @@
     @Override
     public int getPackageStartability(boolean safeMode, @NonNull String packageName, int callingUid,
             @UserIdInt int userId) {
-        final boolean userKeyUnlocked = StorageManager.isUserKeyUnlocked(userId);
+        final boolean ceStorageUnlocked = StorageManager.isCeStorageUnlocked(userId);
         final PackageStateInternal ps = getPackageStateInternal(packageName);
         if (ps == null || shouldFilterApplication(ps, callingUid, userId)
                 || !ps.getUserStateOrDefault(userId).isInstalled()) {
@@ -3571,7 +3571,7 @@
             return PackageManagerService.PACKAGE_STARTABILITY_FROZEN;
         }
 
-        if (!userKeyUnlocked && !AndroidPackageUtils.isEncryptionAware(ps.getPkg())) {
+        if (!ceStorageUnlocked && !AndroidPackageUtils.isEncryptionAware(ps.getPkg())) {
             return PackageManagerService.PACKAGE_STARTABILITY_DIRECT_BOOT_UNSUPPORTED;
         }
         return PackageManagerService.PACKAGE_STARTABILITY_OK;
diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java
index 8bd2982..a10bae9 100644
--- a/services/core/java/com/android/server/pm/DexOptHelper.java
+++ b/services/core/java/com/android/server/pm/DexOptHelper.java
@@ -223,11 +223,6 @@
                 pkgCompilationReason = PackageManagerService.REASON_BACKGROUND_DEXOPT;
             }
 
-            // TODO(b/251903639): Do this when ART Service is used, or remove it from here.
-            if (SystemProperties.getBoolean(mPm.PRECOMPILE_LAYOUTS, false)) {
-                mPm.mArtManagerService.compileLayouts(packageState, pkg);
-            }
-
             int dexoptFlags = bootComplete ? DexoptOptions.DEXOPT_BOOT_COMPLETE : 0;
 
             String filter = getCompilerFilterForReason(pkgCompilationReason);
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 7d716a68..2951ef6 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -59,7 +59,6 @@
 import static com.android.server.pm.PackageManagerService.MIN_INSTALLABLE_TARGET_SDK;
 import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
 import static com.android.server.pm.PackageManagerService.POST_INSTALL;
-import static com.android.server.pm.PackageManagerService.PRECOMPILE_LAYOUTS;
 import static com.android.server.pm.PackageManagerService.SCAN_AS_APEX;
 import static com.android.server.pm.PackageManagerService.SCAN_AS_APK_IN_APEX;
 import static com.android.server.pm.PackageManagerService.SCAN_AS_FACTORY;
@@ -135,7 +134,6 @@
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.SELinux;
-import android.os.SystemProperties;
 import android.os.Trace;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -167,7 +165,6 @@
 import com.android.server.pm.dex.ArtManagerService;
 import com.android.server.pm.dex.DexManager;
 import com.android.server.pm.dex.DexoptOptions;
-import com.android.server.pm.dex.ViewCompiler;
 import com.android.server.pm.parsing.PackageCacher;
 import com.android.server.pm.parsing.PackageParser2;
 import com.android.server.pm.parsing.pkg.AndroidPackageUtils;
@@ -222,7 +219,6 @@
     private final Context mContext;
     private final PackageDexOptimizer mPackageDexOptimizer;
     private final PackageAbiHelper mPackageAbiHelper;
-    private final ViewCompiler mViewCompiler;
     private final SharedLibrariesImpl mSharedLibraries;
     private final PackageManagerServiceInjector mInjector;
     private final UpdateOwnershipHelper mUpdateOwnershipHelper;
@@ -246,7 +242,6 @@
         mContext = pm.mInjector.getContext();
         mPackageDexOptimizer = pm.mInjector.getPackageDexOptimizer();
         mPackageAbiHelper = pm.mInjector.getAbiHelper();
-        mViewCompiler = pm.mInjector.getViewCompiler();
         mSharedLibraries = pm.mInjector.getSharedLibrariesImpl();
         mUpdateOwnershipHelper = pm.mInjector.getUpdateOwnershipHelper();
     }
@@ -2379,12 +2374,6 @@
                 permissionParamsBuilder.setAutoRevokePermissionsMode(autoRevokePermissionsMode);
                 mPm.mPermissionManager.onPackageInstalled(pkg, installRequest.getPreviousAppId(),
                         permissionParamsBuilder.build(), userId);
-                // Apply restricted settings on potentially dangerous packages.
-                if (installRequest.getPackageSource() == PackageInstaller.PACKAGE_SOURCE_LOCAL_FILE
-                        || installRequest.getPackageSource()
-                        == PackageInstaller.PACKAGE_SOURCE_DOWNLOADED_FILE) {
-                    enableRestrictedSettings(pkgName, pkg.getUid());
-                }
             }
             installRequest.setName(pkgName);
             installRequest.setAppId(pkg.getUid());
@@ -2399,16 +2388,13 @@
         Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
     }
 
-    private void enableRestrictedSettings(String pkgName, int appId) {
+    private void enableRestrictedSettings(String pkgName, int appId, int userId) {
         final AppOpsManager appOpsManager = mPm.mContext.getSystemService(AppOpsManager.class);
-        final int[] allUsersList = mPm.mUserManager.getUserIds();
-        for (int userId : allUsersList) {
-            final int uid = UserHandle.getUid(userId, appId);
-            appOpsManager.setMode(AppOpsManager.OP_ACCESS_RESTRICTED_SETTINGS,
-                    uid,
-                    pkgName,
-                    AppOpsManager.MODE_ERRORED);
-        }
+        final int uid = UserHandle.getUid(userId, appId);
+        appOpsManager.setMode(AppOpsManager.OP_ACCESS_RESTRICTED_SETTINGS,
+                uid,
+                pkgName,
+                AppOpsManager.MODE_ERRORED);
     }
 
     /**
@@ -2521,13 +2507,6 @@
                             && !isApex;
 
             if (performDexopt) {
-                // Compile the layout resources.
-                if (SystemProperties.getBoolean(PRECOMPILE_LAYOUTS, false)) {
-                    Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "compileLayouts");
-                    mViewCompiler.compileLayouts(ps, pkg.getBaseApkPath());
-                    Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
-                }
-
                 Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "dexopt");
 
                 // This mirrors logic from commitReconciledScanResultLocked, where the library files
@@ -2832,13 +2811,10 @@
                     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) {
-                    mPm.restorePermissionsAndUpdateRolesForNewUserInstall(packageName,
-                            userId);
-                }
+            for (int userId : firstUserIds) {
+                mPm.restorePermissionsAndUpdateRolesForNewUserInstall(packageName,
+                        userId);
             }
 
             if (request.isAllNewUsers() && !update) {
@@ -2847,6 +2823,16 @@
                 mPm.notifyPackageChanged(packageName, request.getAppId());
             }
 
+            for (int userId : firstUserIds) {
+                // Apply restricted settings on potentially dangerous packages. Needs to happen
+                // after appOpsManager is notified of the new package
+                if (request.getPackageSource() == PackageInstaller.PACKAGE_SOURCE_LOCAL_FILE
+                        || request.getPackageSource()
+                        == PackageInstaller.PACKAGE_SOURCE_DOWNLOADED_FILE) {
+                    enableRestrictedSettings(packageName, request.getAppId(), userId);
+                }
+            }
+
             // Log current value of "unknown sources" setting
             EventLog.writeEvent(EventLogTags.UNKNOWN_SOURCES_ENABLED,
                     getUnknownSourcesSettings());
diff --git a/services/core/java/com/android/server/pm/Installer.java b/services/core/java/com/android/server/pm/Installer.java
index 0ebd33b..4ed3163 100644
--- a/services/core/java/com/android/server/pm/Installer.java
+++ b/services/core/java/com/android/server/pm/Installer.java
@@ -1128,14 +1128,6 @@
         throw new InstallerException("Invalid instruction set: " + instructionSet);
     }
 
-    public boolean compileLayouts(String apkPath, String packageName, String outDexFile, int uid) {
-        try {
-            return mInstalld.compileLayouts(apkPath, packageName, outDexFile, uid);
-        } catch (RemoteException e) {
-            return false;
-        }
-    }
-
     /**
      * Returns the visibility of the optimized artifacts.
      *
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index c260be9..e857e05 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -506,6 +506,9 @@
             if (!canAccessProfile(userId, "cannot get shouldHideFromSuggestions")) {
                 return false;
             }
+            if (Flags.archiving() && packageName != null && isPackageArchived(packageName, user)) {
+                return true;
+            }
             if (mPackageManagerInternal.filterAppAccess(
                     packageName, Binder.getCallingUid(), userId)) {
                 return false;
@@ -758,6 +761,10 @@
             }
         }
 
+        private boolean isPackageArchived(@NonNull String packageName, UserHandle user) {
+            return !getApplicationInfoForArchivedApp(packageName, user).isEmpty();
+        }
+
         @NonNull
         private List<LauncherActivityInfoInternal> generateLauncherActivitiesForArchivedApp(
                 @Nullable String packageName, UserHandle user) {
@@ -969,11 +976,17 @@
             final int callingUid = injectBinderCallingUid();
             final long ident = Binder.clearCallingIdentity();
             try {
-                final PackageInfo info = mPackageManagerInternal.getPackageInfo(packageName,
+                long callingFlag =
                         PackageManager.MATCH_DIRECT_BOOT_AWARE
-                                | PackageManager.MATCH_DIRECT_BOOT_UNAWARE,
-                        callingUid, user.getIdentifier());
-                return info != null && info.applicationInfo.enabled;
+                                | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
+                if (Flags.archiving()) {
+                    callingFlag |= PackageManager.MATCH_ARCHIVED_PACKAGES;
+                }
+                final PackageInfo info =
+                        mPackageManagerInternal.getPackageInfo(
+                                packageName, callingFlag, callingUid, user.getIdentifier());
+                return info != null
+                        && (info.applicationInfo.enabled || info.applicationInfo.isArchived);
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
@@ -1439,7 +1452,18 @@
             if (!canAccessProfile(user.getIdentifier(), "Cannot check component")) {
                 return false;
             }
-
+            if (Flags.archiving() && component != null && component.getPackageName() != null) {
+                List<LauncherActivityInfoInternal> archiveActivities =
+                        generateLauncherActivitiesForArchivedApp(component.getPackageName(), user);
+                if (!archiveActivities.isEmpty()) {
+                    for (int i = 0; i < archiveActivities.size(); i++) {
+                        if (archiveActivities.get(i).getComponentName().equals(component)) {
+                            return true;
+                        }
+                    }
+                    return false;
+                }
+            }
             final int callingUid = injectBinderCallingUid();
             final int state = mPackageManagerInternal.getComponentEnabledSetting(component,
                     callingUid, user.getIdentifier());
diff --git a/services/core/java/com/android/server/pm/MovePackageHelper.java b/services/core/java/com/android/server/pm/MovePackageHelper.java
index 9ad8318..f5f5577 100644
--- a/services/core/java/com/android/server/pm/MovePackageHelper.java
+++ b/services/core/java/com/android/server/pm/MovePackageHelper.java
@@ -196,7 +196,8 @@
         // If we're moving app data around, we need all the users unlocked
         if (moveCompleteApp) {
             for (int userId : installedUserIds) {
-                if (StorageManager.isFileEncrypted() && !StorageManager.isUserKeyUnlocked(userId)) {
+                if (StorageManager.isFileEncrypted()
+                        && !StorageManager.isCeStorageUnlocked(userId)) {
                     freezer.close();
                     throw new PackageManagerException(MOVE_FAILED_LOCKED_USER,
                             "User " + userId + " must be unlocked");
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 52655c4..c9303f2 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -215,7 +215,6 @@
 import com.android.server.pm.dex.ArtUtils;
 import com.android.server.pm.dex.DexManager;
 import com.android.server.pm.dex.DynamicCodeLogger;
-import com.android.server.pm.dex.ViewCompiler;
 import com.android.server.pm.local.PackageManagerLocalImpl;
 import com.android.server.pm.parsing.PackageInfoUtils;
 import com.android.server.pm.parsing.PackageParser2;
@@ -812,8 +811,6 @@
     private final DexManager mDexManager;
     private final DynamicCodeLogger mDynamicCodeLogger;
 
-    final ViewCompiler mViewCompiler;
-
     private final AtomicInteger mNextMoveId = new AtomicInteger();
     final MovePackageHelper.MoveCallbacks mMoveCallbacks;
 
@@ -1673,7 +1670,6 @@
                 (i, pm) -> new ArtManagerService(i.getContext(), i.getInstaller(),
                         i.getInstallLock()),
                 (i, pm) -> ApexManager.getInstance(),
-                (i, pm) -> new ViewCompiler(i.getInstallLock(), i.getInstaller()),
                 (i, pm) -> (IncrementalManager)
                         i.getContext().getSystemService(Context.INCREMENTAL_SERVICE),
                 (i, pm) -> new DefaultAppProvider(() -> context.getSystemService(RoleManager.class),
@@ -1884,7 +1880,6 @@
         mProcessLoggingHandler = testParams.processLoggingHandler;
         mProtectedPackages = testParams.protectedPackages;
         mSeparateProcesses = testParams.separateProcesses;
-        mViewCompiler = testParams.viewCompiler;
         mRequiredVerifierPackages = testParams.requiredVerifierPackages;
         mRequiredInstallerPackage = testParams.requiredInstallerPackage;
         mRequiredUninstallerPackage = testParams.requiredUninstallerPackage;
@@ -2047,7 +2042,6 @@
         mBackgroundDexOptService = injector.getBackgroundDexOptService();
         mArtManagerService = injector.getArtManagerService();
         mMoveCallbacks = new MovePackageHelper.MoveCallbacks(FgThread.get().getLooper());
-        mViewCompiler = injector.getViewCompiler();
         mSharedLibraries = mInjector.getSharedLibrariesImpl();
         mBackgroundHandler = injector.getBackgroundHandler();
 
@@ -3363,7 +3357,7 @@
         UserManagerInternal umInternal = mInjector.getUserManagerInternal();
         StorageManagerInternal smInternal = mInjector.getLocalService(StorageManagerInternal.class);
         final int flags;
-        if (StorageManager.isUserKeyUnlocked(userId) && smInternal.isCeStoragePrepared(userId)) {
+        if (StorageManager.isCeStorageUnlocked(userId) && smInternal.isCeStoragePrepared(userId)) {
             flags = StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE;
         } else if (umInternal.isUserRunning(userId)) {
             flags = StorageManager.FLAG_STORAGE_DE;
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java b/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java
index 5b770aab..ebf1c04 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java
@@ -32,7 +32,6 @@
 import com.android.server.pm.dex.ArtManagerService;
 import com.android.server.pm.dex.DexManager;
 import com.android.server.pm.dex.DynamicCodeLogger;
-import com.android.server.pm.dex.ViewCompiler;
 import com.android.server.pm.parsing.PackageParser2;
 import com.android.server.pm.permission.LegacyPermissionManagerInternal;
 import com.android.server.pm.permission.PermissionManagerServiceInternal;
@@ -112,7 +111,6 @@
     private final Singleton<ArtManagerService>
             mArtManagerServiceProducer;
     private final Singleton<ApexManager> mApexManagerProducer;
-    private final Singleton<ViewCompiler> mViewCompilerProducer;
     private final Singleton<IncrementalManager>
             mIncrementalManagerProducer;
     private final Singleton<DefaultAppProvider>
@@ -164,7 +162,6 @@
             Producer<DynamicCodeLogger> dynamicCodeLoggerProducer,
             Producer<ArtManagerService> artManagerServiceProducer,
             Producer<ApexManager> apexManagerProducer,
-            Producer<ViewCompiler> viewCompilerProducer,
             Producer<IncrementalManager> incrementalManagerProducer,
             Producer<DefaultAppProvider> defaultAppProviderProducer,
             Producer<DisplayMetrics> displayMetricsProducer,
@@ -214,7 +211,6 @@
         mArtManagerServiceProducer = new Singleton<>(
                 artManagerServiceProducer);
         mApexManagerProducer = new Singleton<>(apexManagerProducer);
-        mViewCompilerProducer = new Singleton<>(viewCompilerProducer);
         mIncrementalManagerProducer = new Singleton<>(
                 incrementalManagerProducer);
         mDefaultAppProviderProducer = new Singleton<>(
@@ -339,10 +335,6 @@
         return mApexManagerProducer.get(this, mPackageManager);
     }
 
-    public ViewCompiler getViewCompiler() {
-        return mViewCompilerProducer.get(this, mPackageManager);
-    }
-
     public Handler getBackgroundHandler() {
         return mBackgroundHandler;
     }
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java b/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java
index ca57209..9428ef6 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java
@@ -34,7 +34,6 @@
 import com.android.server.pm.dex.ArtManagerService;
 import com.android.server.pm.dex.DexManager;
 import com.android.server.pm.dex.DynamicCodeLogger;
-import com.android.server.pm.dex.ViewCompiler;
 import com.android.server.pm.parsing.PackageParser2;
 import com.android.server.pm.permission.LegacyPermissionManagerInternal;
 import com.android.server.pm.pkg.AndroidPackage;
@@ -93,7 +92,6 @@
     public @Nullable String systemTextClassifierPackage;
     public @Nullable String overlayConfigSignaturePackage;
     public @NonNull String requiredSdkSandboxPackage;
-    public ViewCompiler viewCompiler;
     public @Nullable String retailDemoPackage;
     public @Nullable String recentsPackage;
     public @Nullable String ambientContextDetectionPackage;
diff --git a/services/core/java/com/android/server/pm/StorageEventHelper.java b/services/core/java/com/android/server/pm/StorageEventHelper.java
index c725cdc..70aa19a 100644
--- a/services/core/java/com/android/server/pm/StorageEventHelper.java
+++ b/services/core/java/com/android/server/pm/StorageEventHelper.java
@@ -183,7 +183,7 @@
                 StorageManagerInternal.class);
         for (UserInfo user : mPm.mUserManager.getUsers(false /* includeDying */)) {
             final int flags;
-            if (StorageManager.isUserKeyUnlocked(user.id)
+            if (StorageManager.isCeStorageUnlocked(user.id)
                     && smInternal.isCeStoragePrepared(user.id)) {
                 flags = StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE;
             } else if (umInternal.isUserRunning(user.id)) {
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 154ee6e..978d8e4 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -1427,7 +1427,7 @@
             }
             final boolean needToShowConfirmCredential = !dontAskCredential
                     && mLockPatternUtils.isSecure(userId)
-                    && (!hasUnifiedChallenge || !StorageManager.isUserKeyUnlocked(userId));
+                    && (!hasUnifiedChallenge || !StorageManager.isCeStorageUnlocked(userId));
             if (needToShowConfirmCredential) {
                 if (onlyIfCredentialNotRequired) {
                     return false;
@@ -5095,9 +5095,9 @@
                 }
             }
 
-            t.traceBegin("createUserKey");
+            t.traceBegin("createUserStorageKeys");
             final StorageManager storage = mContext.getSystemService(StorageManager.class);
-            storage.createUserKey(userId, userInfo.serialNumber, userInfo.isEphemeral());
+            storage.createUserStorageKeys(userId, userInfo.serialNumber, userInfo.isEphemeral());
             t.traceEnd();
 
             // Only prepare DE storage here.  CE storage will be prepared later, when the user is
@@ -5899,17 +5899,18 @@
     private void removeUserState(final @UserIdInt int userId) {
         Slog.i(LOG_TAG, "Removing user state of user " + userId);
 
-        // Cleanup lock settings.  This must happen before destroyUserKey(), since the user's DE
-        // storage must still be accessible for the lock settings state to be properly cleaned up.
+        // Cleanup lock settings.  This requires that the user's DE storage still be accessible, so
+        // this must happen before destroyUserStorageKeys().
         mLockPatternUtils.removeUser(userId);
 
         // Evict and destroy the user's CE and DE encryption keys.  At this point, the user's CE and
         // DE storage is made inaccessible, except to delete its contents.
         try {
-            mContext.getSystemService(StorageManager.class).destroyUserKey(userId);
+            mContext.getSystemService(StorageManager.class).destroyUserStorageKeys(userId);
         } catch (IllegalStateException e) {
             // This may be simply because the user was partially created.
-            Slog.i(LOG_TAG, "Destroying key for user " + userId + " failed, continuing anyway", e);
+            Slog.i(LOG_TAG, "Destroying storage keys for user " + userId
+                    + " failed, continuing anyway", e);
         }
 
         // Cleanup package manager settings
@@ -7177,9 +7178,9 @@
             synchronized (mUserStates) {
                 state = mUserStates.get(userId, -1);
             }
-            // Special case, in the stopping/shutdown state user key can still be unlocked
+            // Special case: in the stopping/shutdown state, CE storage can still be unlocked.
             if (state == UserState.STATE_STOPPING || state == UserState.STATE_SHUTDOWN) {
-                return StorageManager.isUserKeyUnlocked(userId);
+                return StorageManager.isCeStorageUnlocked(userId);
             }
             return (state == UserState.STATE_RUNNING_UNLOCKING)
                     || (state == UserState.STATE_RUNNING_UNLOCKED);
@@ -7196,9 +7197,9 @@
             synchronized (mUserStates) {
                 state = mUserStates.get(userId, -1);
             }
-            // Special case, in the stopping/shutdown state user key can still be unlocked
+            // Special case: in the stopping/shutdown state, CE storage can still be unlocked.
             if (state == UserState.STATE_STOPPING || state == UserState.STATE_SHUTDOWN) {
-                return StorageManager.isUserKeyUnlocked(userId);
+                return StorageManager.isCeStorageUnlocked(userId);
             }
             return state == UserState.STATE_RUNNING_UNLOCKED;
         }
diff --git a/services/core/java/com/android/server/pm/dex/ArtManagerService.java b/services/core/java/com/android/server/pm/dex/ArtManagerService.java
index f4f03f4..ae47aa8 100644
--- a/services/core/java/com/android/server/pm/dex/ArtManagerService.java
+++ b/services/core/java/com/android/server/pm/dex/ArtManagerService.java
@@ -540,44 +540,6 @@
     }
 
     /**
-     * Compile layout resources in a given package.
-     */
-    public boolean compileLayouts(@NonNull PackageStateInternal ps, @NonNull AndroidPackage pkg) {
-        try {
-            if (ps.isPrivileged() || pkg.isUseEmbeddedDex()
-                    || pkg.isDefaultToDeviceProtectedStorage()) {
-                // Privileged apps prefer to load trusted code so they don't use compiled views.
-                // If the app is not privileged but prefers code integrity, also avoid compiling
-                // views.
-                // Also disable the view compiler for protected storage apps since there are
-                // selinux permissions required for writing to user_de.
-                return false;
-            }
-            final String packageName = pkg.getPackageName();
-            final String apkPath = pkg.getSplits().get(0).getPath();
-            final File dataDir = PackageInfoUtils.getDataDir(ps, UserHandle.myUserId());
-            if (dataDir == null) {
-                // The app is not installed on the target user and doesn't have a data dir
-                return false;
-            }
-            final String outDexFile = dataDir.getAbsolutePath() + "/code_cache/compiled_view.dex";
-            Log.i("PackageManager", "Compiling layouts in " + packageName + " (" + apkPath +
-                    ") to " + outDexFile);
-            final long callingId = Binder.clearCallingIdentity();
-            try {
-                return mInstaller.compileLayouts(apkPath, packageName, outDexFile,
-                        pkg.getUid());
-            } finally {
-                Binder.restoreCallingIdentity(callingId);
-            }
-        }
-        catch (Throwable e) {
-            Log.e("PackageManager", "Failed to compile layouts", e);
-            return false;
-        }
-    }
-
-    /**
      * Build the profiles names for all the package code paths (excluding resource only paths).
      * Return the map [code path -> profile name].
      */
diff --git a/services/core/java/com/android/server/pm/dex/ViewCompiler.java b/services/core/java/com/android/server/pm/dex/ViewCompiler.java
deleted file mode 100644
index 00269224..0000000
--- a/services/core/java/com/android/server/pm/dex/ViewCompiler.java
+++ /dev/null
@@ -1,65 +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.server.pm.dex;
-
-import android.os.Binder;
-import android.os.UserHandle;
-import android.util.Log;
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.server.pm.Installer;
-import com.android.server.pm.parsing.PackageInfoUtils;
-import com.android.server.pm.pkg.PackageStateInternal;
-
-import java.io.File;
-
-public class ViewCompiler {
-    private final Object mInstallLock;
-    @GuardedBy("mInstallLock")
-    private final Installer mInstaller;
-
-    public ViewCompiler(Object installLock, Installer installer) {
-        mInstallLock = installLock;
-        mInstaller = installer;
-    }
-
-    public boolean compileLayouts(PackageStateInternal ps, String apkPath) {
-        try {
-            final String packageName = ps.getPackageName();
-            File dataDir = PackageInfoUtils.getDataDir(ps, UserHandle.myUserId());
-            if (dataDir == null) {
-                // The app is not installed on the target user and doesn't have a data dir
-                return false;
-            }
-            final String outDexFile = dataDir.getAbsolutePath() + "/code_cache/compiled_view.dex";
-            Log.i("PackageManager", "Compiling layouts in " + packageName + " (" + apkPath +
-                ") to " + outDexFile);
-            final long callingId = Binder.clearCallingIdentity();
-            try {
-                synchronized (mInstallLock) {
-                    return mInstaller.compileLayouts(apkPath, packageName, outDexFile,
-                        ps.getAppId());
-                }
-            } finally {
-                Binder.restoreCallingIdentity(callingId);
-            }
-        } catch (Throwable e) {
-            Log.e("PackageManager", "Failed to compile layouts", e);
-            return false;
-        }
-    }
-}
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index b439681..077812b 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -546,6 +546,7 @@
     int mLidKeyboardAccessibility;
     int mLidNavigationAccessibility;
     int mShortPressOnPowerBehavior;
+    private boolean mShouldEarlyShortPressOnPower;
     int mLongPressOnPowerBehavior;
     long mLongPressOnPowerAssistantTimeoutMs;
     int mVeryLongPressOnPowerBehavior;
@@ -2651,6 +2652,9 @@
 
         @Override
         void onPress(long downTime) {
+            if (mShouldEarlyShortPressOnPower) {
+                return;
+            }
             powerPress(downTime, 1 /*count*/);
         }
 
@@ -2684,6 +2688,13 @@
         void onMultiPress(long downTime, int count) {
             powerPress(downTime, count);
         }
+
+        @Override
+        void onKeyUp(long eventTime, int count) {
+            if (mShouldEarlyShortPressOnPower && count == 1) {
+                powerPress(eventTime, 1 /*pressCount*/);
+            }
+        }
     }
 
     /**
@@ -2913,6 +2924,9 @@
                     Settings.Global.STEM_PRIMARY_BUTTON_LONG_PRESS,
                     mContext.getResources().getInteger(
                             com.android.internal.R.integer.config_longPressOnStemPrimaryBehavior));
+            mShouldEarlyShortPressOnPower =
+                    mContext.getResources()
+                            .getBoolean(com.android.internal.R.bool.config_shortPressEarlyOnPower);
 
             mStylusButtonsEnabled = Settings.Secure.getIntForUser(resolver,
                     Secure.STYLUS_BUTTONS_ENABLED, 1, UserHandle.USER_CURRENT) == 1;
diff --git a/services/core/java/com/android/server/power/stats/AggregatedPowerStats.java b/services/core/java/com/android/server/power/stats/AggregatedPowerStats.java
index bc90f5c..aadd03b 100644
--- a/services/core/java/com/android/server/power/stats/AggregatedPowerStats.java
+++ b/services/core/java/com/android/server/power/stats/AggregatedPowerStats.java
@@ -19,7 +19,6 @@
 import android.annotation.CurrentTimeMillisLong;
 import android.annotation.DurationMillisLong;
 import android.annotation.NonNull;
-import android.os.BatteryConsumer;
 import android.os.UserHandle;
 import android.text.format.DateFormat;
 import android.util.IndentingPrintWriter;
@@ -66,17 +65,7 @@
                 aggregatedPowerStatsConfig.getPowerComponentsAggregatedStatsConfigs();
         mPowerComponentStats = new PowerComponentAggregatedPowerStats[configs.size()];
         for (int i = 0; i < configs.size(); i++) {
-            mPowerComponentStats[i] = createPowerComponentAggregatedPowerStats(configs.get(i));
-        }
-    }
-
-    private PowerComponentAggregatedPowerStats createPowerComponentAggregatedPowerStats(
-            AggregatedPowerStatsConfig.PowerComponent config) {
-        switch (config.getPowerComponentId()) {
-            case BatteryConsumer.POWER_COMPONENT_CPU:
-                return new CpuAggregatedPowerStats(config);
-            default:
-                return new PowerComponentAggregatedPowerStats(config);
+            mPowerComponentStats[i] = new PowerComponentAggregatedPowerStats(configs.get(i));
         }
     }
 
diff --git a/services/core/java/com/android/server/power/stats/AggregatedPowerStatsConfig.java b/services/core/java/com/android/server/power/stats/AggregatedPowerStatsConfig.java
index 477c228..43fd15d 100644
--- a/services/core/java/com/android/server/power/stats/AggregatedPowerStatsConfig.java
+++ b/services/core/java/com/android/server/power/stats/AggregatedPowerStatsConfig.java
@@ -16,13 +16,16 @@
 package com.android.server.power.stats;
 
 import android.annotation.IntDef;
+import android.annotation.NonNull;
 import android.os.BatteryConsumer;
 
 import com.android.internal.os.MultiStateStats;
+import com.android.internal.os.PowerStats;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 /**
@@ -73,6 +76,7 @@
         private final int mPowerComponentId;
         private @TrackedState int[] mTrackedDeviceStates;
         private @TrackedState int[] mTrackedUidStates;
+        private AggregatedPowerStatsProcessor mProcessor = NO_OP_PROCESSOR;
 
         PowerComponent(int powerComponentId) {
             this.mPowerComponentId = powerComponentId;
@@ -94,6 +98,16 @@
             return this;
         }
 
+        /**
+         * Takes an object that should be invoked for every aggregated stats span
+         * before giving the aggregates stats to consumers. The processor can complete the
+         * aggregation process, for example by computing estimated power usage.
+         */
+        public PowerComponent setProcessor(@NonNull AggregatedPowerStatsProcessor processor) {
+            mProcessor = processor;
+            return this;
+        }
+
         public int getPowerComponentId() {
             return mPowerComponentId;
         }
@@ -123,6 +137,11 @@
             };
         }
 
+        @NonNull
+        public AggregatedPowerStatsProcessor getProcessor() {
+            return mProcessor;
+        }
+
         private boolean isTracked(int[] trackedStates, int state) {
             if (trackedStates == null) {
                 return false;
@@ -153,4 +172,21 @@
     public List<PowerComponent> getPowerComponentsAggregatedStatsConfigs() {
         return mPowerComponents;
     }
+
+    private static final AggregatedPowerStatsProcessor NO_OP_PROCESSOR =
+            new AggregatedPowerStatsProcessor() {
+                @Override
+                public void finish(PowerComponentAggregatedPowerStats stats) {
+                }
+
+                @Override
+                public String deviceStatsToString(PowerStats.Descriptor descriptor, long[] stats) {
+                    return Arrays.toString(stats);
+                }
+
+                @Override
+                public String uidStatsToString(PowerStats.Descriptor descriptor, long[] stats) {
+                    return Arrays.toString(stats);
+                }
+            };
 }
diff --git a/services/core/java/com/android/server/power/stats/AggregatedPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/AggregatedPowerStatsProcessor.java
new file mode 100644
index 0000000..5fd8ddf
--- /dev/null
+++ b/services/core/java/com/android/server/power/stats/AggregatedPowerStatsProcessor.java
@@ -0,0 +1,348 @@
+/*
+ * 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.power.stats;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Log;
+
+import com.android.internal.os.MultiStateStats;
+import com.android.internal.os.PowerStats;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/*
+ * The power estimation algorithm used by AggregatedPowerStatsProcessor can roughly be
+ * described like this:
+ *
+ * 1. Estimate power usage for each state combination (e.g. power-battery/screen-on) using
+ * a metric such as CPU time-in-state.
+ *
+ * 2. Combine estimates obtain in step 1, aggregating across states that are *not* tracked
+ * per UID.
+ *
+ * 2. For each UID, compute the proportion of the combined estimates in each state
+ * and attribute the corresponding portion of the total power estimate in that state to the UID.
+ */
+abstract class AggregatedPowerStatsProcessor {
+    private static final String TAG = "AggregatedPowerStatsProcessor";
+
+    private static final int INDEX_DOES_NOT_EXIST = -1;
+    private static final double MILLIAMPHOUR_PER_MICROCOULOMB = 1.0 / 1000.0 / 60.0 / 60.0;
+
+    abstract void finish(PowerComponentAggregatedPowerStats stats);
+
+    abstract String deviceStatsToString(PowerStats.Descriptor descriptor, long[] stats);
+
+    abstract String uidStatsToString(PowerStats.Descriptor descriptor, long[] stats);
+
+    protected static class PowerEstimationPlan {
+        private final AggregatedPowerStatsConfig.PowerComponent mConfig;
+        public List<DeviceStateEstimation> deviceStateEstimations = new ArrayList<>();
+        public List<CombinedDeviceStateEstimate> combinedDeviceStateEstimations = new ArrayList<>();
+        public List<UidStateEstimate> uidStateEstimates = new ArrayList<>();
+
+        public PowerEstimationPlan(AggregatedPowerStatsConfig.PowerComponent config) {
+            mConfig = config;
+            addDeviceStateEstimations();
+            combineDeviceStateEstimations();
+            addUidStateEstimations();
+        }
+
+        private void addDeviceStateEstimations() {
+            MultiStateStats.States[] config = mConfig.getDeviceStateConfig();
+            int[][] deviceStateCombinations = getAllTrackedStateCombinations(config);
+            for (int[] deviceStateCombination : deviceStateCombinations) {
+                deviceStateEstimations.add(
+                        new DeviceStateEstimation(config, deviceStateCombination));
+            }
+        }
+
+        private void combineDeviceStateEstimations() {
+            MultiStateStats.States[] deviceStateConfig = mConfig.getDeviceStateConfig();
+            MultiStateStats.States[] uidStateConfig = mConfig.getUidStateConfig();
+            MultiStateStats.States[] deviceStatesTrackedPerUid =
+                    new MultiStateStats.States[deviceStateConfig.length];
+
+            for (int i = 0; i < deviceStateConfig.length; i++) {
+                if (!deviceStateConfig[i].isTracked()) {
+                    continue;
+                }
+
+                int index = findTrackedStateByName(uidStateConfig, deviceStateConfig[i].getName());
+                if (index != INDEX_DOES_NOT_EXIST && uidStateConfig[index].isTracked()) {
+                    deviceStatesTrackedPerUid[i] = deviceStateConfig[i];
+                }
+            }
+
+            combineDeviceStateEstimationsRecursively(deviceStateConfig, deviceStatesTrackedPerUid,
+                    new int[deviceStateConfig.length], 0);
+        }
+
+        private void combineDeviceStateEstimationsRecursively(
+                MultiStateStats.States[] deviceStateConfig,
+                MultiStateStats.States[] deviceStatesTrackedPerUid, int[] stateValues, int state) {
+            if (state >= deviceStateConfig.length) {
+                DeviceStateEstimation dse = getDeviceStateEstimate(stateValues);
+                CombinedDeviceStateEstimate cdse = getCombinedDeviceStateEstimate(
+                        deviceStatesTrackedPerUid, stateValues);
+                if (cdse == null) {
+                    cdse = new CombinedDeviceStateEstimate(deviceStatesTrackedPerUid, stateValues);
+                    combinedDeviceStateEstimations.add(cdse);
+                }
+                cdse.deviceStateEstimations.add(dse);
+                return;
+            }
+
+            if (deviceStateConfig[state].isTracked()) {
+                for (int stateValue = 0;
+                        stateValue < deviceStateConfig[state].getLabels().length;
+                        stateValue++) {
+                    stateValues[state] = stateValue;
+                    combineDeviceStateEstimationsRecursively(deviceStateConfig,
+                            deviceStatesTrackedPerUid, stateValues, state + 1);
+                }
+            } else {
+                combineDeviceStateEstimationsRecursively(deviceStateConfig,
+                        deviceStatesTrackedPerUid, stateValues, state + 1);
+            }
+        }
+
+        private void addUidStateEstimations() {
+            MultiStateStats.States[] deviceStateConfig = mConfig.getDeviceStateConfig();
+            MultiStateStats.States[] uidStateConfig = mConfig.getUidStateConfig();
+            MultiStateStats.States[] uidStatesTrackedForDevice =
+                    new MultiStateStats.States[uidStateConfig.length];
+            MultiStateStats.States[] uidStatesNotTrackedForDevice =
+                    new MultiStateStats.States[uidStateConfig.length];
+
+            for (int i = 0; i < uidStateConfig.length; i++) {
+                if (!uidStateConfig[i].isTracked()) {
+                    continue;
+                }
+
+                int index = findTrackedStateByName(deviceStateConfig, uidStateConfig[i].getName());
+                if (index != INDEX_DOES_NOT_EXIST && deviceStateConfig[index].isTracked()) {
+                    uidStatesTrackedForDevice[i] = uidStateConfig[i];
+                } else {
+                    uidStatesNotTrackedForDevice[i] = uidStateConfig[i];
+                }
+            }
+
+            @AggregatedPowerStatsConfig.TrackedState
+            int[][] uidStateCombinations = getAllTrackedStateCombinations(uidStateConfig);
+            for (int[] stateValues : uidStateCombinations) {
+                CombinedDeviceStateEstimate combined =
+                        getCombinedDeviceStateEstimate(uidStatesTrackedForDevice, stateValues);
+                if (combined == null) {
+                    // This is not supposed to be possible
+                    Log.wtf(TAG, "Mismatch in UID and combined device states: "
+                                 + concatLabels(uidStatesTrackedForDevice, stateValues));
+                    continue;
+                }
+                UidStateEstimate uidStateEstimate = getUidStateEstimate(combined);
+                if (uidStateEstimate == null) {
+                    uidStateEstimate = new UidStateEstimate(combined, uidStatesNotTrackedForDevice);
+                    uidStateEstimates.add(uidStateEstimate);
+                }
+                uidStateEstimate.proportionalEstimates.add(
+                        new UidStateProportionalEstimate(stateValues));
+            }
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append("Step 1. Compute device-wide power estimates for state combinations:\n");
+            for (DeviceStateEstimation deviceStateEstimation : deviceStateEstimations) {
+                sb.append("    ").append(deviceStateEstimation.id).append("\n");
+            }
+            sb.append("Step 2. Combine device-wide estimates that are untracked per UID:\n");
+            boolean any = false;
+            for (CombinedDeviceStateEstimate cdse : combinedDeviceStateEstimations) {
+                if (cdse.deviceStateEstimations.size() <= 1) {
+                    continue;
+                }
+                any = true;
+                sb.append("    ").append(cdse.id).append(": ");
+                for (int i = 0; i < cdse.deviceStateEstimations.size(); i++) {
+                    if (i != 0) {
+                        sb.append(" + ");
+                    }
+                    sb.append(cdse.deviceStateEstimations.get(i).id);
+                }
+                sb.append("\n");
+            }
+            if (!any) {
+                sb.append("    N/A\n");
+            }
+            sb.append("Step 3. Proportionally distribute power estimates to UIDs:\n");
+            for (UidStateEstimate uidStateEstimate : uidStateEstimates) {
+                sb.append("    ").append(uidStateEstimate.combinedDeviceStateEstimate.id)
+                        .append("\n        among: ");
+                for (int i = 0; i < uidStateEstimate.proportionalEstimates.size(); i++) {
+                    UidStateProportionalEstimate uspe =
+                            uidStateEstimate.proportionalEstimates.get(i);
+                    if (i != 0) {
+                        sb.append(", ");
+                    }
+                    sb.append(concatLabels(uidStateEstimate.states, uspe.stateValues));
+                }
+                sb.append("\n");
+            }
+            return sb.toString();
+        }
+
+        @Nullable
+        public DeviceStateEstimation getDeviceStateEstimate(
+                @AggregatedPowerStatsConfig.TrackedState int[] stateValues) {
+            String label = concatLabels(mConfig.getDeviceStateConfig(), stateValues);
+            for (int i = 0; i < deviceStateEstimations.size(); i++) {
+                DeviceStateEstimation deviceStateEstimation = this.deviceStateEstimations.get(i);
+                if (deviceStateEstimation.id.equals(label)) {
+                    return deviceStateEstimation;
+                }
+            }
+            return null;
+        }
+
+        public CombinedDeviceStateEstimate getCombinedDeviceStateEstimate(
+                MultiStateStats.States[] deviceStates,
+                @AggregatedPowerStatsConfig.TrackedState int[] stateValues) {
+            String label = concatLabels(deviceStates, stateValues);
+            for (int i = 0; i < combinedDeviceStateEstimations.size(); i++) {
+                CombinedDeviceStateEstimate cdse = combinedDeviceStateEstimations.get(i);
+                if (cdse.id.equals(label)) {
+                    return cdse;
+                }
+            }
+            return null;
+        }
+
+        public UidStateEstimate getUidStateEstimate(CombinedDeviceStateEstimate combined) {
+            for (int i = 0; i < uidStateEstimates.size(); i++) {
+                UidStateEstimate uidStateEstimate = uidStateEstimates.get(i);
+                if (uidStateEstimate.combinedDeviceStateEstimate == combined) {
+                    return uidStateEstimate;
+                }
+            }
+            return null;
+        }
+
+        public void resetIntermediates() {
+            for (int i = deviceStateEstimations.size() - 1; i >= 0; i--) {
+                deviceStateEstimations.get(i).intermediates = null;
+            }
+            for (int i = deviceStateEstimations.size() - 1; i >= 0; i--) {
+                deviceStateEstimations.get(i).intermediates = null;
+            }
+            for (int i = uidStateEstimates.size() - 1; i >= 0; i--) {
+                UidStateEstimate uidStateEstimate = uidStateEstimates.get(i);
+                List<UidStateProportionalEstimate> proportionalEstimates =
+                        uidStateEstimate.proportionalEstimates;
+                for (int j = proportionalEstimates.size() - 1; j >= 0; j--) {
+                    proportionalEstimates.get(j).intermediates = null;
+                }
+            }
+        }
+    }
+
+    protected static class DeviceStateEstimation {
+        public final String id;
+        @AggregatedPowerStatsConfig.TrackedState
+        public final int[] stateValues;
+        public Object intermediates;
+
+        public DeviceStateEstimation(MultiStateStats.States[] config,
+                @AggregatedPowerStatsConfig.TrackedState int[] stateValues) {
+            id = concatLabels(config, stateValues);
+            this.stateValues = stateValues;
+        }
+    }
+
+    protected static class CombinedDeviceStateEstimate {
+        public final String id;
+        public List<DeviceStateEstimation> deviceStateEstimations = new ArrayList<>();
+        public Object intermediates;
+
+        public CombinedDeviceStateEstimate(MultiStateStats.States[] config,
+                @AggregatedPowerStatsConfig.TrackedState int[] stateValues) {
+            id = concatLabels(config, stateValues);
+        }
+    }
+
+    protected static class UidStateEstimate {
+        public final MultiStateStats.States[] states;
+        public CombinedDeviceStateEstimate combinedDeviceStateEstimate;
+        public List<UidStateProportionalEstimate> proportionalEstimates = new ArrayList<>();
+
+        public UidStateEstimate(CombinedDeviceStateEstimate combined,
+                MultiStateStats.States[] states) {
+            combinedDeviceStateEstimate = combined;
+            this.states = states;
+        }
+    }
+
+    protected static class UidStateProportionalEstimate {
+        @AggregatedPowerStatsConfig.TrackedState
+        public final int[] stateValues;
+        public Object intermediates;
+
+        protected UidStateProportionalEstimate(
+                @AggregatedPowerStatsConfig.TrackedState int[] stateValues) {
+            this.stateValues = stateValues;
+        }
+    }
+
+    private static int findTrackedStateByName(MultiStateStats.States[] states, String name) {
+        for (int i = 0; i < states.length; i++) {
+            if (states[i].getName().equals(name)) {
+                return i;
+            }
+        }
+        return INDEX_DOES_NOT_EXIST;
+    }
+
+    @NonNull
+    private static String concatLabels(MultiStateStats.States[] config,
+            @AggregatedPowerStatsConfig.TrackedState int[] stateValues) {
+        List<String> labels = new ArrayList<>();
+        for (int state = 0; state < config.length; state++) {
+            if (config[state] != null && config[state].isTracked()) {
+                labels.add(config[state].getName()
+                           + "=" + config[state].getLabels()[stateValues[state]]);
+            }
+        }
+        Collections.sort(labels);
+        return labels.toString();
+    }
+
+    @AggregatedPowerStatsConfig.TrackedState
+    private static int[][] getAllTrackedStateCombinations(MultiStateStats.States[] states) {
+        List<int[]> combinations = new ArrayList<>();
+        MultiStateStats.States.forEachTrackedStateCombination(states, stateValues -> {
+            combinations.add(Arrays.copyOf(stateValues, stateValues.length));
+        });
+        return combinations.toArray(new int[combinations.size()][0]);
+    }
+
+    public static double uCtoMah(long chargeUC) {
+        return chargeUC * MILLIAMPHOUR_PER_MICROCOULOMB;
+    }
+}
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index a9c2bc2..a6558e0 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -10937,7 +10937,8 @@
         }
 
         mCpuPowerStatsCollector = new CpuPowerStatsCollector(mCpuScalingPolicies, mPowerProfile,
-                mHandler, mBatteryStatsConfig.getPowerStatsThrottlePeriodCpu());
+                () -> mBatteryVoltageMv, mHandler,
+                mBatteryStatsConfig.getPowerStatsThrottlePeriodCpu());
         mCpuPowerStatsCollector.addConsumer(this::recordPowerStats);
 
         mStartCount++;
@@ -14437,6 +14438,7 @@
             final int level, /* not final */ int temp, final int voltageMv, final int chargeUah,
             final int chargeFullUah, final long chargeTimeToFullSeconds,
             final long elapsedRealtimeMs, final long uptimeMs, final long currentTimeMs) {
+
         // Temperature is encoded without the signed bit, so clamp any negative temperatures to 0.
         temp = Math.max(0, temp);
 
@@ -15621,18 +15623,6 @@
         }
     }
 
-    @GuardedBy("this")
-    private void dumpCpuPowerBracketsLocked(PrintWriter pw) {
-        pw.println("CPU power brackets; cluster/freq in MHz(avg current in mA):");
-        final int bracketCount = mPowerProfile.getCpuPowerBracketCount();
-        for (int bracket = 0; bracket < bracketCount; bracket++) {
-            pw.print("    ");
-            pw.print(bracket);
-            pw.print(": ");
-            pw.println(mPowerProfile.getCpuPowerBracketDescription(mCpuScalingPolicies, bracket));
-        }
-    }
-
     /**
      * Dump EnergyConsumer stats
      */
@@ -16989,8 +16979,10 @@
             pw.println();
             dumpConstantsLocked(pw);
 
-            pw.println();
-            dumpCpuPowerBracketsLocked(pw);
+            if (mCpuPowerStatsCollector != null) {
+                pw.println();
+                mCpuPowerStatsCollector.dumpCpuPowerBracketsLocked(pw);
+            }
 
             pw.println();
             dumpEnergyConsumerStatsLocked(pw);
diff --git a/services/core/java/com/android/server/power/stats/CpuAggregatedPowerStats.java b/services/core/java/com/android/server/power/stats/CpuAggregatedPowerStats.java
deleted file mode 100644
index fbf6928..0000000
--- a/services/core/java/com/android/server/power/stats/CpuAggregatedPowerStats.java
+++ /dev/null
@@ -1,23 +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.power.stats;
-
-class CpuAggregatedPowerStats extends PowerComponentAggregatedPowerStats {
-    CpuAggregatedPowerStats(AggregatedPowerStatsConfig.PowerComponent config) {
-        super(config);
-    }
-}
diff --git a/services/core/java/com/android/server/power/stats/CpuAggregatedPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/CpuAggregatedPowerStatsProcessor.java
new file mode 100644
index 0000000..f40eef2
--- /dev/null
+++ b/services/core/java/com/android/server/power/stats/CpuAggregatedPowerStatsProcessor.java
@@ -0,0 +1,545 @@
+/*
+ * 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.power.stats;
+
+import android.os.BatteryStats;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.os.CpuScalingPolicies;
+import com.android.internal.os.PowerProfile;
+import com.android.internal.os.PowerStats;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+public class CpuAggregatedPowerStatsProcessor extends AggregatedPowerStatsProcessor {
+    private static final String TAG = "CpuAggregatedPowerStatsProcessor";
+
+    private static final double HOUR_IN_MILLIS = TimeUnit.HOURS.toMillis(1);
+    private static final int UNKNOWN = -1;
+
+    private final CpuScalingPolicies mCpuScalingPolicies;
+    // Number of CPU core clusters
+    private final int mCpuClusterCount;
+    // Total number of CPU scaling steps across all clusters
+    private final int mCpuScalingStepCount;
+    // Map of scaling step to the corresponding core cluster mScalingStepToCluster[step]->cluster
+    private final int[] mScalingStepToCluster;
+    // Average power consumed by the CPU when it is powered up (per power_profile.xml)
+    private final double mPowerMultiplierForCpuActive;
+    // Average power consumed by each cluster when it is powered up (per power_profile.xml)
+    private final double[] mPowerMultipliersByCluster;
+    // Average power consumed by each scaling step when running code (per power_profile.xml)
+    private final double[] mPowerMultipliersByScalingStep;
+    // A map used to combine energy consumers into a smaller set, in case power brackets
+    // are defined in a way that does not allow an unambiguous mapping of energy consumers to
+    // brackets
+    private int[] mEnergyConsumerToCombinedEnergyConsumerMap;
+    // A map of combined energy consumers to the corresponding collections of power brackets.
+    // For example, if there are two CPU_CLUSTER rails and each maps to three brackets,
+    // this map will look like this:
+    //     0 : [0, 1, 2]
+    //     1 : [3, 4, 5]
+    private int[][] mCombinedEnergyConsumerToPowerBracketMap;
+
+    // Cached reference to a PowerStats descriptor. Almost never changes in practice,
+    // helping to avoid reparsing the descriptor for every PowerStats span.
+    private PowerStats.Descriptor mLastUsedDescriptor;
+    // Cached results of parsing of current PowerStats.Descriptor. Only refreshed when
+    // mLastUsedDescriptor changes
+    private CpuPowerStatsCollector.StatsArrayLayout mStatsLayout;
+    // Sequence of steps for power estimation and intermediate results.
+    private PowerEstimationPlan mPlan;
+
+    // Temp array for retrieval of device power stats, to avoid repeated allocations
+    private long[] mTmpDeviceStatsArray;
+    // Temp array for retrieval of UID power stats, to avoid repeated allocations
+    private long[] mTmpUidStatsArray;
+
+    public CpuAggregatedPowerStatsProcessor(PowerProfile powerProfile,
+            CpuScalingPolicies scalingPolicies) {
+        mCpuScalingPolicies = scalingPolicies;
+        mCpuScalingStepCount = scalingPolicies.getScalingStepCount();
+        mScalingStepToCluster = new int[mCpuScalingStepCount];
+        mPowerMultipliersByScalingStep = new double[mCpuScalingStepCount];
+
+        int step = 0;
+        int[] policies = scalingPolicies.getPolicies();
+        mCpuClusterCount = policies.length;
+        mPowerMultipliersByCluster = new double[mCpuClusterCount];
+        for (int cluster = 0; cluster < mCpuClusterCount; cluster++) {
+            int policy = policies[cluster];
+            mPowerMultipliersByCluster[cluster] =
+                    powerProfile.getAveragePowerForCpuScalingPolicy(policy) / HOUR_IN_MILLIS;
+            int[] frequencies = scalingPolicies.getFrequencies(policy);
+            for (int i = 0; i < frequencies.length; i++) {
+                mScalingStepToCluster[step] = cluster;
+                mPowerMultipliersByScalingStep[step] =
+                        powerProfile.getAveragePowerForCpuScalingStep(policy, i) / HOUR_IN_MILLIS;
+                step++;
+            }
+        }
+        mPowerMultiplierForCpuActive =
+                powerProfile.getAveragePower(PowerProfile.POWER_CPU_ACTIVE) / HOUR_IN_MILLIS;
+    }
+
+    private void unpackPowerStatsDescriptor(PowerStats.Descriptor descriptor) {
+        if (descriptor.equals(mLastUsedDescriptor)) {
+            return;
+        }
+
+        mLastUsedDescriptor = descriptor;
+        mStatsLayout = new CpuPowerStatsCollector.StatsArrayLayout();
+        mStatsLayout.fromExtras(descriptor.extras);
+
+        mTmpDeviceStatsArray = new long[descriptor.statsArrayLength];
+        mTmpUidStatsArray = new long[descriptor.uidStatsArrayLength];
+    }
+
+    /**
+     * Temporary struct to capture intermediate results of power estimation.
+     */
+    private static final class Intermediates {
+        public long uptime;
+        // Sum of all time-in-step values, combining all time-in-step durations across all cores.
+        public long cumulativeTime;
+        // CPU activity durations per cluster
+        public long[] timeByCluster;
+        // Sums of time-in-step values, aggregated by cluster, combining all cores in the cluster.
+        public long[] cumulativeTimeByCluster;
+        public long[] timeByScalingStep;
+        public double[] powerByCluster;
+        public double[] powerByScalingStep;
+        public long[] powerByEnergyConsumer;
+    }
+
+    /**
+     * Temporary struct to capture intermediate results of power estimation.
+     */
+    private static class DeviceStatsIntermediates {
+        public double power;
+        public long[] timeByBracket;
+        public double[] powerByBracket;
+    }
+
+    @Override
+    public void finish(PowerComponentAggregatedPowerStats stats) {
+        if (stats.getPowerStatsDescriptor() == null) {
+            return;
+        }
+
+        unpackPowerStatsDescriptor(stats.getPowerStatsDescriptor());
+
+        if (mPlan == null) {
+            mPlan = new PowerEstimationPlan(stats.getConfig());
+            if (mStatsLayout.getCpuClusterEnergyConsumerCount() != 0) {
+                initEnergyConsumerToPowerBracketMaps();
+            }
+        }
+
+        Intermediates intermediates = new Intermediates();
+
+        int cpuScalingStepCount = mStatsLayout.getCpuScalingStepCount();
+        if (cpuScalingStepCount != mCpuScalingStepCount) {
+            Log.e(TAG, "Mismatched CPU scaling step count in PowerStats: " + cpuScalingStepCount
+                       + ", expected: " + mCpuScalingStepCount);
+            return;
+        }
+
+        int clusterCount = mStatsLayout.getCpuClusterCount();
+        if (clusterCount != mCpuClusterCount) {
+            Log.e(TAG, "Mismatched CPU cluster count in PowerStats: " + clusterCount
+                       + ", expected: " + mCpuClusterCount);
+            return;
+        }
+
+        computeTotals(stats, intermediates);
+        if (intermediates.cumulativeTime == 0) {
+            return;
+        }
+
+        estimatePowerByScalingStep(intermediates);
+        estimatePowerByDeviceState(stats, intermediates);
+        combineDeviceStateEstimates();
+
+        ArrayList<Integer> uids = new ArrayList<>();
+        stats.collectUids(uids);
+        if (!uids.isEmpty()) {
+            for (int uid : uids) {
+                for (int i = 0; i < mPlan.uidStateEstimates.size(); i++) {
+                    estimateUidPowerConsumption(stats, uid, mPlan.uidStateEstimates.get(i));
+                }
+            }
+        }
+        mPlan.resetIntermediates();
+    }
+
+    /*
+     * Populate data structures (two maps) needed to use power rail data, aka energy consumers,
+     * to attribute power usage to apps.
+     *
+     * At this point, the algorithm covers only the most basic cases:
+     * - Each cluster is mapped to unique power brackets (possibly multiple for each cluster):
+     *          CL_0: [bracket0, bracket1]
+     *          CL_1: [bracket3]
+     *      In this case, the consumed energy is distributed  to the corresponding brackets
+     *      proportionally.
+     * - Brackets span multiple clusters:
+     *          CL_0: [bracket0, bracket1]
+     *          CL_1: [bracket1, bracket2]
+     *          CL_2: [bracket3, bracket4]
+     *      In this case, we combine energy consumers into groups unambiguously mapped to
+     *      brackets. In the above example, consumed energy for CL_0 and CL_1 will be combined
+     *      because they both map to the same power bracket (bracket1):
+     *          (CL_0+CL_1): [bracket0, bracket1, bracket2]
+     *          CL_2: [bracket3, bracket4]
+     */
+    private void initEnergyConsumerToPowerBracketMaps() {
+        int energyConsumerCount = mStatsLayout.getCpuClusterEnergyConsumerCount();
+        int powerBracketCount = mStatsLayout.getCpuPowerBracketCount();
+
+        mEnergyConsumerToCombinedEnergyConsumerMap = new int[energyConsumerCount];
+        mCombinedEnergyConsumerToPowerBracketMap = new int[energyConsumerCount][];
+
+        int[] policies = mCpuScalingPolicies.getPolicies();
+        if (energyConsumerCount == policies.length) {
+            int[] scalingStepToPowerBracketMap = mStatsLayout.getScalingStepToPowerBracketMap();
+            ArraySet<Integer>[] clusterToBrackets = new ArraySet[policies.length];
+            int step = 0;
+            for (int cluster = 0; cluster < policies.length; cluster++) {
+                int[] freqs = mCpuScalingPolicies.getFrequencies(policies[cluster]);
+                clusterToBrackets[cluster] = new ArraySet<>(freqs.length);
+                for (int j = 0; j < freqs.length; j++) {
+                    clusterToBrackets[cluster].add(scalingStepToPowerBracketMap[step++]);
+                }
+            }
+
+            ArraySet<Integer>[] combinedEnergyConsumers = new ArraySet[policies.length];
+            int combinedEnergyConsumersCount = 0;
+
+            for (int cluster = 0; cluster < clusterToBrackets.length; cluster++) {
+                int combineWith = UNKNOWN;
+                for (int i = 0; i < combinedEnergyConsumersCount; i++) {
+                    if (containsAny(combinedEnergyConsumers[i], clusterToBrackets[cluster])) {
+                        combineWith = i;
+                        break;
+                    }
+                }
+                if (combineWith != UNKNOWN) {
+                    mEnergyConsumerToCombinedEnergyConsumerMap[cluster] = combineWith;
+                    combinedEnergyConsumers[combineWith].addAll(clusterToBrackets[cluster]);
+                } else {
+                    mEnergyConsumerToCombinedEnergyConsumerMap[cluster] =
+                            combinedEnergyConsumersCount;
+                    combinedEnergyConsumers[combinedEnergyConsumersCount++] =
+                            clusterToBrackets[cluster];
+                }
+            }
+
+            for (int i = combinedEnergyConsumers.length - 1; i >= 0; i--) {
+                mCombinedEnergyConsumerToPowerBracketMap[i] =
+                        new int[combinedEnergyConsumers[i].size()];
+                for (int j = combinedEnergyConsumers[i].size() - 1; j >= 0; j--) {
+                    mCombinedEnergyConsumerToPowerBracketMap[i][j] =
+                            combinedEnergyConsumers[i].valueAt(j);
+                }
+            }
+        } else {
+            // All CPU cluster energy consumers are combined into one, which is
+            // distributed proportionally to all power brackets.
+            int[] map = new int[powerBracketCount];
+            for (int i = 0; i < map.length; i++) {
+                map[i] = i;
+            }
+            mCombinedEnergyConsumerToPowerBracketMap[0] = map;
+        }
+    }
+
+    private static boolean containsAny(ArraySet<Integer> set1, ArraySet<Integer> set2) {
+        for (int i = 0; i < set2.size(); i++) {
+            if (set1.contains(set2.valueAt(i))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void computeTotals(PowerComponentAggregatedPowerStats stats,
+            Intermediates intermediates) {
+        intermediates.timeByScalingStep = new long[mCpuScalingStepCount];
+        intermediates.timeByCluster = new long[mCpuClusterCount];
+        intermediates.cumulativeTimeByCluster = new long[mCpuClusterCount];
+
+        List<DeviceStateEstimation> deviceStateEstimations = mPlan.deviceStateEstimations;
+        for (int i = deviceStateEstimations.size() - 1; i >= 0; i--) {
+            DeviceStateEstimation deviceStateEstimation = deviceStateEstimations.get(i);
+            if (!stats.getDeviceStats(mTmpDeviceStatsArray, deviceStateEstimation.stateValues)) {
+                continue;
+            }
+
+            intermediates.uptime += mStatsLayout.getUptime(mTmpDeviceStatsArray);
+
+            for (int cluster = 0; cluster < mCpuClusterCount; cluster++) {
+                intermediates.timeByCluster[cluster] +=
+                        mStatsLayout.getTimeByCluster(mTmpDeviceStatsArray, cluster);
+            }
+
+            for (int step = 0; step < mCpuScalingStepCount; step++) {
+                long timeInStep = mStatsLayout.getTimeByScalingStep(mTmpDeviceStatsArray, step);
+                intermediates.cumulativeTime += timeInStep;
+                intermediates.timeByScalingStep[step] += timeInStep;
+                intermediates.cumulativeTimeByCluster[mScalingStepToCluster[step]] += timeInStep;
+            }
+        }
+    }
+
+    private void estimatePowerByScalingStep(Intermediates intermediates) {
+        // CPU consumes some power when it's on - no matter which cores are running.
+        double cpuActivePower = mPowerMultiplierForCpuActive * intermediates.uptime;
+
+        // Additionally, every cluster consumes some power when any of its cores are running
+        intermediates.powerByCluster = new double[mCpuClusterCount];
+        for (int cluster = 0; cluster < mCpuClusterCount; cluster++) {
+            intermediates.powerByCluster[cluster] =
+                    mPowerMultipliersByCluster[cluster] * intermediates.timeByCluster[cluster];
+        }
+
+        // Finally, additional power is consumed depending on the frequency scaling
+        intermediates.powerByScalingStep = new double[mCpuScalingStepCount];
+        for (int step = 0; step < mCpuScalingStepCount; step++) {
+            int cluster = mScalingStepToCluster[step];
+
+            double power;
+
+            // Distribute base power proportionally
+            power = cpuActivePower * intermediates.timeByScalingStep[step]
+                    / intermediates.cumulativeTime;
+
+            // Distribute per-cluster power proportionally
+            long cumulativeTimeInCluster = intermediates.cumulativeTimeByCluster[cluster];
+            if (cumulativeTimeInCluster != 0) {
+                power += intermediates.powerByCluster[cluster]
+                         * intermediates.timeByScalingStep[step]
+                         / cumulativeTimeInCluster;
+            }
+
+            power += mPowerMultipliersByScalingStep[step] * intermediates.timeByScalingStep[step];
+
+            intermediates.powerByScalingStep[step] = power;
+        }
+    }
+
+    private void estimatePowerByDeviceState(PowerComponentAggregatedPowerStats stats,
+            Intermediates intermediates) {
+        int cpuScalingStepCount = mStatsLayout.getCpuScalingStepCount();
+        int powerBracketCount = mStatsLayout.getCpuPowerBracketCount();
+        int[] scalingStepToBracketMap = mStatsLayout.getScalingStepToPowerBracketMap();
+        int energyConsumerCount = mStatsLayout.getCpuClusterEnergyConsumerCount();
+        List<DeviceStateEstimation> deviceStateEstimations = mPlan.deviceStateEstimations;
+        for (int dse = deviceStateEstimations.size() - 1; dse >= 0; dse--) {
+            DeviceStateEstimation deviceStateEstimation = deviceStateEstimations.get(dse);
+            deviceStateEstimation.intermediates = new DeviceStatsIntermediates();
+            DeviceStatsIntermediates deviceStatsIntermediates =
+                    (DeviceStatsIntermediates) deviceStateEstimation.intermediates;
+            deviceStatsIntermediates.timeByBracket = new long[powerBracketCount];
+            deviceStatsIntermediates.powerByBracket = new double[powerBracketCount];
+
+            stats.getDeviceStats(mTmpDeviceStatsArray, deviceStateEstimation.stateValues);
+            for (int step = 0; step < cpuScalingStepCount; step++) {
+                if (intermediates.timeByScalingStep[step] == 0) {
+                    continue;
+                }
+
+                long timeInStep = mStatsLayout.getTimeByScalingStep(mTmpDeviceStatsArray, step);
+                double stepPower = intermediates.powerByScalingStep[step] * timeInStep
+                                   / intermediates.timeByScalingStep[step];
+
+                int bracket = scalingStepToBracketMap[step];
+                deviceStatsIntermediates.timeByBracket[bracket] += timeInStep;
+                deviceStatsIntermediates.powerByBracket[bracket] += stepPower;
+            }
+
+            if (energyConsumerCount != 0) {
+                adjustEstimatesUsingEnergyConsumers(intermediates, deviceStatsIntermediates);
+            }
+
+            double power = 0;
+            for (int i = deviceStatsIntermediates.powerByBracket.length - 1; i >= 0; i--) {
+                power += deviceStatsIntermediates.powerByBracket[i];
+            }
+            deviceStatsIntermediates.power = power;
+            mStatsLayout.setDevicePowerEstimate(mTmpDeviceStatsArray, power);
+            stats.setDeviceStats(deviceStateEstimation.stateValues, mTmpDeviceStatsArray);
+        }
+    }
+
+    private void adjustEstimatesUsingEnergyConsumers(
+            Intermediates intermediates, DeviceStatsIntermediates deviceStatsIntermediates) {
+        int energyConsumerCount = mStatsLayout.getCpuClusterEnergyConsumerCount();
+        if (energyConsumerCount == 0) {
+            return;
+        }
+
+        if (intermediates.powerByEnergyConsumer == null) {
+            intermediates.powerByEnergyConsumer = new long[energyConsumerCount];
+        } else {
+            Arrays.fill(intermediates.powerByEnergyConsumer, 0);
+        }
+        for (int i = 0; i < energyConsumerCount; i++) {
+            intermediates.powerByEnergyConsumer[mEnergyConsumerToCombinedEnergyConsumerMap[i]] +=
+                    mStatsLayout.getConsumedEnergy(mTmpDeviceStatsArray, i);
+        }
+
+        for (int combinedConsumer = mCombinedEnergyConsumerToPowerBracketMap.length - 1;
+                combinedConsumer >= 0; combinedConsumer--) {
+            int[] combinedEnergyConsumerToPowerBracketMap =
+                    mCombinedEnergyConsumerToPowerBracketMap[combinedConsumer];
+            if (combinedEnergyConsumerToPowerBracketMap == null) {
+                continue;
+            }
+
+            double consumedEnergy = uCtoMah(intermediates.powerByEnergyConsumer[combinedConsumer]);
+
+            double totalModeledPower = 0;
+            for (int bracket : combinedEnergyConsumerToPowerBracketMap) {
+                totalModeledPower += deviceStatsIntermediates.powerByBracket[bracket];
+            }
+            if (totalModeledPower == 0) {
+                continue;
+            }
+
+            for (int bracket : combinedEnergyConsumerToPowerBracketMap) {
+                deviceStatsIntermediates.powerByBracket[bracket] =
+                        consumedEnergy * deviceStatsIntermediates.powerByBracket[bracket]
+                        / totalModeledPower;
+            }
+        }
+    }
+
+    private void combineDeviceStateEstimates() {
+        for (int i = mPlan.combinedDeviceStateEstimations.size() - 1; i >= 0; i--) {
+            CombinedDeviceStateEstimate cdse = mPlan.combinedDeviceStateEstimations.get(i);
+            DeviceStatsIntermediates cdseIntermediates = new DeviceStatsIntermediates();
+            cdse.intermediates = cdseIntermediates;
+            int bracketCount = mStatsLayout.getCpuPowerBracketCount();
+            cdseIntermediates.timeByBracket = new long[bracketCount];
+            cdseIntermediates.powerByBracket = new double[bracketCount];
+            List<DeviceStateEstimation> deviceStateEstimations = cdse.deviceStateEstimations;
+            for (int j = deviceStateEstimations.size() - 1; j >= 0; j--) {
+                DeviceStateEstimation dse = deviceStateEstimations.get(j);
+                DeviceStatsIntermediates intermediates =
+                        (DeviceStatsIntermediates) dse.intermediates;
+                cdseIntermediates.power += intermediates.power;
+                for (int k = 0; k < bracketCount; k++) {
+                    cdseIntermediates.timeByBracket[k] += intermediates.timeByBracket[k];
+                    cdseIntermediates.powerByBracket[k] += intermediates.powerByBracket[k];
+                }
+            }
+        }
+    }
+
+    private void estimateUidPowerConsumption(PowerComponentAggregatedPowerStats stats, int uid,
+            UidStateEstimate uidStateEstimate) {
+        CombinedDeviceStateEstimate combinedDeviceStateEstimate =
+                uidStateEstimate.combinedDeviceStateEstimate;
+        DeviceStatsIntermediates cdsIntermediates =
+                (DeviceStatsIntermediates) combinedDeviceStateEstimate.intermediates;
+        for (int i = 0; i < uidStateEstimate.proportionalEstimates.size(); i++) {
+            UidStateProportionalEstimate proportionalEstimate =
+                    uidStateEstimate.proportionalEstimates.get(i);
+            if (!stats.getUidStats(mTmpUidStatsArray, uid, proportionalEstimate.stateValues)) {
+                continue;
+            }
+
+            double power = 0;
+            for (int bracket = 0; bracket < mStatsLayout.getCpuPowerBracketCount(); bracket++) {
+                if (cdsIntermediates.timeByBracket[bracket] == 0) {
+                    continue;
+                }
+
+                long timeInBracket = mStatsLayout.getUidTimeByPowerBracket(mTmpUidStatsArray,
+                        bracket);
+                if (timeInBracket == 0) {
+                    continue;
+                }
+
+                power += cdsIntermediates.powerByBracket[bracket] * timeInBracket
+                            / cdsIntermediates.timeByBracket[bracket];
+            }
+
+            mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power);
+            stats.setUidStats(uid, proportionalEstimate.stateValues, mTmpUidStatsArray);
+        }
+    }
+
+    @Override
+    public String deviceStatsToString(PowerStats.Descriptor descriptor, long[] stats) {
+        unpackPowerStatsDescriptor(descriptor);
+        StringBuilder sb = new StringBuilder();
+        int cpuScalingStepCount = mStatsLayout.getCpuScalingStepCount();
+        sb.append("steps: [");
+        for (int step = 0; step < cpuScalingStepCount; step++) {
+            if (step != 0) {
+                sb.append(", ");
+            }
+            sb.append(mStatsLayout.getTimeByScalingStep(stats, step));
+        }
+        int clusterCount = mStatsLayout.getCpuClusterCount();
+        sb.append("] clusters: [");
+        for (int cluster = 0; cluster < clusterCount; cluster++) {
+            if (cluster != 0) {
+                sb.append(", ");
+            }
+            sb.append(mStatsLayout.getTimeByCluster(stats, cluster));
+        }
+        sb.append("] uptime: ").append(mStatsLayout.getUptime(stats));
+        int energyConsumerCount = mStatsLayout.getCpuClusterEnergyConsumerCount();
+        if (energyConsumerCount > 0) {
+            sb.append(" energy: [");
+            for (int i = 0; i < energyConsumerCount; i++) {
+                if (i != 0) {
+                    sb.append(", ");
+                }
+                sb.append(mStatsLayout.getConsumedEnergy(stats, i));
+            }
+            sb.append("]");
+        }
+        sb.append(" power: ").append(
+                BatteryStats.formatCharge(mStatsLayout.getDevicePowerEstimate(stats)));
+        return sb.toString();
+    }
+
+    @Override
+    public String uidStatsToString(PowerStats.Descriptor descriptor, long[] stats) {
+        unpackPowerStatsDescriptor(descriptor);
+        StringBuilder sb = new StringBuilder();
+        sb.append("[");
+        int powerBracketCount = mStatsLayout.getCpuPowerBracketCount();
+        for (int bracket = 0; bracket < powerBracketCount; bracket++) {
+            if (bracket != 0) {
+                sb.append(", ");
+            }
+            sb.append(mStatsLayout.getUidTimeByPowerBracket(stats, bracket));
+        }
+        sb.append("] power: ").append(
+                BatteryStats.formatCharge(mStatsLayout.getUidPowerEstimate(stats)));
+        return sb.toString();
+    }
+}
diff --git a/services/core/java/com/android/server/power/stats/CpuPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/CpuPowerStatsCollector.java
index 376ca89..a388932 100644
--- a/services/core/java/com/android/server/power/stats/CpuPowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/CpuPowerStatsCollector.java
@@ -16,19 +16,39 @@
 
 package com.android.server.power.stats;
 
+import android.hardware.power.stats.EnergyConsumer;
+import android.hardware.power.stats.EnergyConsumerResult;
+import android.hardware.power.stats.EnergyConsumerType;
 import android.os.BatteryConsumer;
 import android.os.Handler;
 import android.os.PersistableBundle;
+import android.power.PowerStatsInternal;
+import android.util.Slog;
 import android.util.SparseArray;
 
 import com.android.internal.annotations.Keep;
 import com.android.internal.annotations.VisibleForNative;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.Clock;
 import com.android.internal.os.CpuScalingPolicies;
 import com.android.internal.os.PowerProfile;
 import com.android.internal.os.PowerStats;
+import com.android.server.LocalServices;
 import com.android.server.power.optimization.Flags;
 
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.IntSupplier;
+import java.util.function.Supplier;
+
 /**
  * Collects snapshots of power-related system statistics.
  * <p>
@@ -36,45 +56,350 @@
  * constructor. Thus the object is not thread-safe except where noted.
  */
 public class CpuPowerStatsCollector extends PowerStatsCollector {
+    private static final String TAG = "CpuPowerStatsCollector";
     private static final long NANOS_PER_MILLIS = 1000000;
+    private static final long ENERGY_UNSPECIFIED = -1;
+    private static final int DEFAULT_CPU_POWER_BRACKETS = 3;
+    private static final int DEFAULT_CPU_POWER_BRACKETS_PER_ENERGY_CONSUMER = 2;
+    private static final long POWER_STATS_ENERGY_CONSUMERS_TIMEOUT = 20000;
 
+    private boolean mIsInitialized;
+    private final CpuScalingPolicies mCpuScalingPolicies;
+    private final PowerProfile mPowerProfile;
     private final KernelCpuStatsReader mKernelCpuStatsReader;
-    private final int[] mScalingStepToPowerBracketMap;
-    private final long[] mTempUidStats;
+    private final Supplier<PowerStatsInternal> mPowerStatsSupplier;
+    private final IntSupplier mVoltageSupplier;
+    private final int mDefaultCpuPowerBrackets;
+    private final int mDefaultCpuPowerBracketsPerEnergyConsumer;
+    private long[] mCpuTimeByScalingStep;
+    private long[] mTempCpuTimeByScalingStep;
+    private long[] mTempUidStats;
     private final SparseArray<UidStats> mUidStats = new SparseArray<>();
-    private final int mUidStatsSize;
+    private boolean mIsPerUidTimeInStateSupported;
+    private PowerStatsInternal mPowerStatsInternal;
+    private int[] mCpuEnergyConsumerIds;
+    private PowerStats.Descriptor mPowerStatsDescriptor;
     // Reusable instance
-    private final PowerStats mCpuPowerStats;
+    private PowerStats mCpuPowerStats;
+    private StatsArrayLayout mLayout;
     private long mLastUpdateTimestampNanos;
+    private long mLastUpdateUptimeMillis;
+    private int mLastVoltageMv;
+    private long[] mLastConsumedEnergyUws;
+
+    /**
+     * Captures the positions and lengths of sections of the stats array, such as time-in-state,
+     * power usage estimates etc.
+     */
+    public static class StatsArrayLayout {
+        private static final String EXTRA_DEVICE_TIME_BY_SCALING_STEP_POSITION = "dt";
+        private static final String EXTRA_DEVICE_TIME_BY_SCALING_STEP_COUNT = "dtc";
+        private static final String EXTRA_DEVICE_TIME_BY_CLUSTER_POSITION = "dc";
+        private static final String EXTRA_DEVICE_TIME_BY_CLUSTER_COUNT = "dcc";
+        private static final String EXTRA_DEVICE_POWER_POSITION = "dp";
+        private static final String EXTRA_DEVICE_UPTIME_POSITION = "du";
+        private static final String EXTRA_DEVICE_ENERGY_CONSUMERS_POSITION = "de";
+        private static final String EXTRA_DEVICE_ENERGY_CONSUMERS_COUNT = "dec";
+        private static final String EXTRA_UID_BRACKETS_POSITION = "ub";
+        private static final String EXTRA_UID_STATS_SCALING_STEP_TO_POWER_BRACKET = "us";
+        private static final String EXTRA_UID_POWER_POSITION = "up";
+
+        private static final double MILLI_TO_NANO_MULTIPLIER = 1000000.0;
+
+        private int mDeviceStatsArrayLength;
+        private int mUidStatsArrayLength;
+
+        private int mDeviceCpuTimeByScalingStepPosition;
+        private int mDeviceCpuTimeByScalingStepCount;
+        private int mDeviceCpuTimeByClusterPosition;
+        private int mDeviceCpuTimeByClusterCount;
+        private int mDeviceCpuUptimePosition;
+        private int mDeviceEnergyConsumerPosition;
+        private int mDeviceEnergyConsumerCount;
+        private int mDevicePowerEstimatePosition;
+
+        private int mUidPowerBracketsPosition;
+        private int mUidPowerBracketCount;
+        private int[][] mEnergyConsumerToPowerBucketMaps;
+        private int mUidPowerEstimatePosition;
+
+        private int[] mScalingStepToPowerBracketMap;
+
+        public int getDeviceStatsArrayLength() {
+            return mDeviceStatsArrayLength;
+        }
+
+        public int getUidStatsArrayLength() {
+            return mUidStatsArrayLength;
+        }
+
+        /**
+         * Declare that the stats array has a section capturing CPU time per scaling step
+         */
+        public void addDeviceSectionCpuTimeByScalingStep(int scalingStepCount) {
+            mDeviceCpuTimeByScalingStepPosition = mDeviceStatsArrayLength;
+            mDeviceCpuTimeByScalingStepCount = scalingStepCount;
+            mDeviceStatsArrayLength += scalingStepCount;
+        }
+
+        public int getCpuScalingStepCount() {
+            return mDeviceCpuTimeByScalingStepCount;
+        }
+
+        /**
+         * Saves the time duration in the <code>stats</code> element
+         * corresponding to the CPU scaling <code>state</code>.
+         */
+        public void setTimeByScalingStep(long[] stats, int step, long value) {
+            stats[mDeviceCpuTimeByScalingStepPosition + step] = value;
+        }
+
+        /**
+         * Extracts the time duration from the <code>stats</code> element
+         * corresponding to the CPU scaling <code>step</code>.
+         */
+        public long getTimeByScalingStep(long[] stats, int step) {
+            return stats[mDeviceCpuTimeByScalingStepPosition + step];
+        }
+
+        /**
+         * Declare that the stats array has a section capturing CPU time in each cluster
+         */
+        public void addDeviceSectionCpuTimeByCluster(int clusterCount) {
+            mDeviceCpuTimeByClusterCount = clusterCount;
+            mDeviceCpuTimeByClusterPosition = mDeviceStatsArrayLength;
+            mDeviceStatsArrayLength += clusterCount;
+        }
+
+        public int getCpuClusterCount() {
+            return mDeviceCpuTimeByClusterCount;
+        }
+
+        /**
+         * Saves the time duration in the <code>stats</code> element
+         * corresponding to the CPU <code>cluster</code>.
+         */
+        public void setTimeByCluster(long[] stats, int cluster, long value) {
+            stats[mDeviceCpuTimeByClusterPosition + cluster] = value;
+        }
+
+        /**
+         * Extracts the time duration from the <code>stats</code> element
+         * corresponding to the CPU <code>cluster</code>.
+         */
+        public long getTimeByCluster(long[] stats, int cluster) {
+            return stats[mDeviceCpuTimeByClusterPosition + cluster];
+        }
+
+        /**
+         * Declare that the stats array has a section capturing CPU uptime
+         */
+        public void addDeviceSectionUptime() {
+            mDeviceCpuUptimePosition = mDeviceStatsArrayLength++;
+        }
+
+        /**
+         * Saves the CPU uptime duration in the corresponding <code>stats</code> element.
+         */
+        public void setUptime(long[] stats, long value) {
+            stats[mDeviceCpuUptimePosition] = value;
+        }
+
+        /**
+         * Extracts the CPU uptime duration from the corresponding <code>stats</code> element.
+         */
+        public long getUptime(long[] stats) {
+            return stats[mDeviceCpuUptimePosition];
+        }
+
+        /**
+         * Declares that the stats array has a section capturing EnergyConsumer data from
+         * PowerStatsService.
+         */
+        public void addDeviceSectionEnergyConsumers(int energyConsumerCount) {
+            mDeviceEnergyConsumerPosition = mDeviceStatsArrayLength;
+            mDeviceEnergyConsumerCount = energyConsumerCount;
+            mDeviceStatsArrayLength += energyConsumerCount;
+        }
+
+        public int getCpuClusterEnergyConsumerCount() {
+            return mDeviceEnergyConsumerCount;
+        }
+
+        /**
+         * Saves the accumulated energy for the specified rail the corresponding
+         * <code>stats</code> element.
+         */
+        public void setConsumedEnergy(long[] stats, int index, long energy) {
+            stats[mDeviceEnergyConsumerPosition + index] = energy;
+        }
+
+        /**
+         * Extracts the EnergyConsumer data from a device stats array for the specified
+         * EnergyConsumer.
+         */
+        public long getConsumedEnergy(long[] stats, int index) {
+            return stats[mDeviceEnergyConsumerPosition + index];
+        }
+
+        /**
+         * Declare that the stats array has a section capturing a power estimate
+         */
+        public void addDeviceSectionPowerEstimate() {
+            mDevicePowerEstimatePosition = mDeviceStatsArrayLength++;
+        }
+
+        /**
+         * Converts the supplied mAh power estimate to a long and saves it in the corresponding
+         * element of <code>stats</code>.
+         */
+        public void setDevicePowerEstimate(long[] stats, double power) {
+            stats[mDevicePowerEstimatePosition] = (long) (power * MILLI_TO_NANO_MULTIPLIER);
+        }
+
+        /**
+         * Extracts the power estimate from a device stats array and converts it to mAh.
+         */
+        public double getDevicePowerEstimate(long[] stats) {
+            return stats[mDevicePowerEstimatePosition] / MILLI_TO_NANO_MULTIPLIER;
+        }
+
+        /**
+         * Declare that the UID stats array has a section capturing CPU time per power bracket.
+         */
+        public void addUidSectionCpuTimeByPowerBracket(int[] scalingStepToPowerBracketMap) {
+            mScalingStepToPowerBracketMap = scalingStepToPowerBracketMap;
+            mUidPowerBracketsPosition = mUidStatsArrayLength;
+            updatePowerBracketCount();
+            mUidStatsArrayLength += mUidPowerBracketCount;
+        }
+
+        private void updatePowerBracketCount() {
+            mUidPowerBracketCount = 1;
+            for (int bracket : mScalingStepToPowerBracketMap) {
+                if (bracket >= mUidPowerBracketCount) {
+                    mUidPowerBracketCount = bracket + 1;
+                }
+            }
+        }
+
+        public int[] getScalingStepToPowerBracketMap() {
+            return mScalingStepToPowerBracketMap;
+        }
+
+        public int getCpuPowerBracketCount() {
+            return mUidPowerBracketCount;
+        }
+
+        /**
+         * Saves time in <code>bracket</code> in the corresponding section of <code>stats</code>.
+         */
+        public void setUidTimeByPowerBracket(long[] stats, int bracket, long value) {
+            stats[mUidPowerBracketsPosition + bracket] = value;
+        }
+
+        /**
+         * Extracts the time in <code>bracket</code> from a UID stats array.
+         */
+        public long getUidTimeByPowerBracket(long[] stats, int bracket) {
+            return stats[mUidPowerBracketsPosition + bracket];
+        }
+
+        /**
+         * Declare that the UID stats array has a section capturing a power estimate
+         */
+        public void addUidSectionPowerEstimate() {
+            mUidPowerEstimatePosition = mUidStatsArrayLength++;
+        }
+
+        /**
+         * Converts the supplied mAh power estimate to a long and saves it in the corresponding
+         * element of <code>stats</code>.
+         */
+        public void setUidPowerEstimate(long[] stats, double power) {
+            stats[mUidPowerEstimatePosition] = (long) (power * MILLI_TO_NANO_MULTIPLIER);
+        }
+
+        /**
+         * Extracts the power estimate from a UID stats array and converts it to mAh.
+         */
+        public double getUidPowerEstimate(long[] stats) {
+            return stats[mUidPowerEstimatePosition] / MILLI_TO_NANO_MULTIPLIER;
+        }
+
+        /**
+         * Copies the elements of the stats array layout into <code>extras</code>
+         */
+        public void toExtras(PersistableBundle extras) {
+            extras.putInt(EXTRA_DEVICE_TIME_BY_SCALING_STEP_POSITION,
+                    mDeviceCpuTimeByScalingStepPosition);
+            extras.putInt(EXTRA_DEVICE_TIME_BY_SCALING_STEP_COUNT,
+                    mDeviceCpuTimeByScalingStepCount);
+            extras.putInt(EXTRA_DEVICE_TIME_BY_CLUSTER_POSITION,
+                    mDeviceCpuTimeByClusterPosition);
+            extras.putInt(EXTRA_DEVICE_TIME_BY_CLUSTER_COUNT,
+                    mDeviceCpuTimeByClusterCount);
+            extras.putInt(EXTRA_DEVICE_UPTIME_POSITION, mDeviceCpuUptimePosition);
+            extras.putInt(EXTRA_DEVICE_ENERGY_CONSUMERS_POSITION,
+                    mDeviceEnergyConsumerPosition);
+            extras.putInt(EXTRA_DEVICE_ENERGY_CONSUMERS_COUNT,
+                    mDeviceEnergyConsumerCount);
+            extras.putInt(EXTRA_DEVICE_POWER_POSITION, mDevicePowerEstimatePosition);
+            extras.putInt(EXTRA_UID_BRACKETS_POSITION, mUidPowerBracketsPosition);
+            extras.putIntArray(EXTRA_UID_STATS_SCALING_STEP_TO_POWER_BRACKET,
+                    mScalingStepToPowerBracketMap);
+            extras.putInt(EXTRA_UID_POWER_POSITION, mUidPowerEstimatePosition);
+        }
+
+        /**
+         * Retrieves elements of the stats array layout from <code>extras</code>
+         */
+        public void fromExtras(PersistableBundle extras) {
+            mDeviceCpuTimeByScalingStepPosition =
+                    extras.getInt(EXTRA_DEVICE_TIME_BY_SCALING_STEP_POSITION);
+            mDeviceCpuTimeByScalingStepCount =
+                    extras.getInt(EXTRA_DEVICE_TIME_BY_SCALING_STEP_COUNT);
+            mDeviceCpuTimeByClusterPosition =
+                    extras.getInt(EXTRA_DEVICE_TIME_BY_CLUSTER_POSITION);
+            mDeviceCpuTimeByClusterCount =
+                    extras.getInt(EXTRA_DEVICE_TIME_BY_CLUSTER_COUNT);
+            mDeviceCpuUptimePosition = extras.getInt(EXTRA_DEVICE_UPTIME_POSITION);
+            mDeviceEnergyConsumerPosition = extras.getInt(EXTRA_DEVICE_ENERGY_CONSUMERS_POSITION);
+            mDeviceEnergyConsumerCount = extras.getInt(EXTRA_DEVICE_ENERGY_CONSUMERS_COUNT);
+            mDevicePowerEstimatePosition = extras.getInt(EXTRA_DEVICE_POWER_POSITION);
+            mUidPowerBracketsPosition = extras.getInt(EXTRA_UID_BRACKETS_POSITION);
+            mScalingStepToPowerBracketMap =
+                    extras.getIntArray(EXTRA_UID_STATS_SCALING_STEP_TO_POWER_BRACKET);
+            if (mScalingStepToPowerBracketMap == null) {
+                mScalingStepToPowerBracketMap = new int[mDeviceCpuTimeByScalingStepCount];
+            }
+            updatePowerBracketCount();
+            mUidPowerEstimatePosition = extras.getInt(EXTRA_UID_POWER_POSITION);
+        }
+    }
 
     public CpuPowerStatsCollector(CpuScalingPolicies cpuScalingPolicies, PowerProfile powerProfile,
-                                  Handler handler, long throttlePeriodMs) {
+            IntSupplier voltageSupplier, Handler handler, long throttlePeriodMs) {
         this(cpuScalingPolicies, powerProfile, handler, new KernelCpuStatsReader(),
-                throttlePeriodMs, Clock.SYSTEM_CLOCK);
+                () -> LocalServices.getService(PowerStatsInternal.class), voltageSupplier,
+                throttlePeriodMs, Clock.SYSTEM_CLOCK, DEFAULT_CPU_POWER_BRACKETS,
+                DEFAULT_CPU_POWER_BRACKETS_PER_ENERGY_CONSUMER);
     }
 
     public CpuPowerStatsCollector(CpuScalingPolicies cpuScalingPolicies, PowerProfile powerProfile,
                                   Handler handler, KernelCpuStatsReader kernelCpuStatsReader,
-                                  long throttlePeriodMs, Clock clock) {
+            Supplier<PowerStatsInternal> powerStatsSupplier,
+            IntSupplier voltageSupplier, long throttlePeriodMs, Clock clock,
+            int defaultCpuPowerBrackets, int defaultCpuPowerBracketsPerEnergyConsumer) {
         super(handler, throttlePeriodMs, clock);
+        mCpuScalingPolicies = cpuScalingPolicies;
+        mPowerProfile = powerProfile;
         mKernelCpuStatsReader = kernelCpuStatsReader;
+        mPowerStatsSupplier = powerStatsSupplier;
+        mVoltageSupplier = voltageSupplier;
+        mDefaultCpuPowerBrackets = defaultCpuPowerBrackets;
+        mDefaultCpuPowerBracketsPerEnergyConsumer = defaultCpuPowerBracketsPerEnergyConsumer;
 
-        int scalingStepCount = cpuScalingPolicies.getScalingStepCount();
-        mScalingStepToPowerBracketMap = new int[scalingStepCount];
-        int index = 0;
-        for (int policy : cpuScalingPolicies.getPolicies()) {
-            int[] frequencies = cpuScalingPolicies.getFrequencies(policy);
-            for (int step = 0; step < frequencies.length; step++) {
-                int bracket = powerProfile.getCpuPowerBracketForScalingStep(policy, step);
-                mScalingStepToPowerBracketMap[index++] = bracket;
-            }
-        }
-        mUidStatsSize = powerProfile.getCpuPowerBracketCount();
-        mTempUidStats = new long[mUidStatsSize];
-
-        mCpuPowerStats = new PowerStats(
-                new PowerStats.Descriptor(BatteryConsumer.POWER_COMPONENT_CPU, 0, mUidStatsSize,
-                        new PersistableBundle()));
     }
 
     /**
@@ -84,43 +409,355 @@
         setEnabled(Flags.streamlinedBatteryStats());
     }
 
+    private void ensureInitialized() {
+        if (mIsInitialized) {
+            return;
+        }
+
+        if (!isEnabled()) {
+            return;
+        }
+
+        mIsPerUidTimeInStateSupported = mKernelCpuStatsReader.nativeIsSupportedFeature();
+        mPowerStatsInternal = mPowerStatsSupplier.get();
+
+        if (mPowerStatsInternal != null) {
+            readCpuEnergyConsumerIds();
+        } else {
+            mCpuEnergyConsumerIds = new int[0];
+        }
+
+        int cpuScalingStepCount = mCpuScalingPolicies.getScalingStepCount();
+        mCpuTimeByScalingStep = new long[cpuScalingStepCount];
+        mTempCpuTimeByScalingStep = new long[cpuScalingStepCount];
+        int[] scalingStepToPowerBracketMap = initPowerBrackets();
+
+        mLayout = new StatsArrayLayout();
+        mLayout.addDeviceSectionCpuTimeByScalingStep(cpuScalingStepCount);
+        mLayout.addDeviceSectionCpuTimeByCluster(mCpuScalingPolicies.getPolicies().length);
+        mLayout.addDeviceSectionUptime();
+        mLayout.addDeviceSectionEnergyConsumers(mCpuEnergyConsumerIds.length);
+        mLayout.addDeviceSectionPowerEstimate();
+        mLayout.addUidSectionCpuTimeByPowerBracket(scalingStepToPowerBracketMap);
+        mLayout.addUidSectionPowerEstimate();
+
+        PersistableBundle extras = new PersistableBundle();
+        mLayout.toExtras(extras);
+
+        mPowerStatsDescriptor = new PowerStats.Descriptor(BatteryConsumer.POWER_COMPONENT_CPU,
+                mLayout.getDeviceStatsArrayLength(), mLayout.getUidStatsArrayLength(), extras);
+        mCpuPowerStats = new PowerStats(mPowerStatsDescriptor);
+
+        mTempUidStats = new long[mLayout.getCpuPowerBracketCount()];
+
+        mIsInitialized = true;
+    }
+
+    private void readCpuEnergyConsumerIds() {
+        EnergyConsumer[] energyConsumerInfo = mPowerStatsInternal.getEnergyConsumerInfo();
+        if (energyConsumerInfo == null) {
+            mCpuEnergyConsumerIds = new int[0];
+            return;
+        }
+
+        List<EnergyConsumer> cpuEnergyConsumers = new ArrayList<>();
+        for (EnergyConsumer energyConsumer : energyConsumerInfo) {
+            if (energyConsumer.type == EnergyConsumerType.CPU_CLUSTER) {
+                cpuEnergyConsumers.add(energyConsumer);
+            }
+        }
+        if (cpuEnergyConsumers.isEmpty()) {
+            return;
+        }
+
+        cpuEnergyConsumers.sort(Comparator.comparing(c -> c.ordinal));
+
+        mCpuEnergyConsumerIds = new int[cpuEnergyConsumers.size()];
+        for (int i = 0; i < mCpuEnergyConsumerIds.length; i++) {
+            mCpuEnergyConsumerIds[i] = cpuEnergyConsumers.get(i).id;
+        }
+        mLastConsumedEnergyUws = new long[cpuEnergyConsumers.size()];
+        Arrays.fill(mLastConsumedEnergyUws, ENERGY_UNSPECIFIED);
+    }
+
+    private int[] initPowerBrackets() {
+        if (mPowerProfile.getCpuPowerBracketCount() != PowerProfile.POWER_BRACKETS_UNSPECIFIED) {
+            return initPowerBracketsFromPowerProfile();
+        } else if (mCpuEnergyConsumerIds.length == 0 || mCpuEnergyConsumerIds.length == 1) {
+            return initDefaultPowerBrackets(mDefaultCpuPowerBrackets);
+        } else if (mCpuScalingPolicies.getPolicies().length == mCpuEnergyConsumerIds.length) {
+            return initPowerBracketsByCluster(mDefaultCpuPowerBracketsPerEnergyConsumer);
+        } else {
+            Slog.i(TAG, "Assigning a single power brackets to each CPU_CLUSTER energy consumer."
+                        + " Number of CPU clusters ("
+                        + mCpuScalingPolicies.getPolicies().length
+                        + ") does not match the number of energy consumers ("
+                        + mCpuEnergyConsumerIds.length + "). "
+                        + " Using default power bucket assignment.");
+            return initDefaultPowerBrackets(mDefaultCpuPowerBrackets);
+        }
+    }
+
+    private int[] initPowerBracketsFromPowerProfile() {
+        int[] stepToBracketMap = new int[mCpuScalingPolicies.getScalingStepCount()];
+        int index = 0;
+        for (int policy : mCpuScalingPolicies.getPolicies()) {
+            int[] frequencies = mCpuScalingPolicies.getFrequencies(policy);
+            for (int step = 0; step < frequencies.length; step++) {
+                int bracket = mPowerProfile.getCpuPowerBracketForScalingStep(policy, step);
+                stepToBracketMap[index++] = bracket;
+            }
+        }
+        return stepToBracketMap;
+    }
+
+    private int[] initPowerBracketsByCluster(int defaultBracketCountPerCluster) {
+        int[] stepToBracketMap = new int[mCpuScalingPolicies.getScalingStepCount()];
+        int index = 0;
+        int bracketBase = 0;
+        int[] policies = mCpuScalingPolicies.getPolicies();
+        for (int policy : policies) {
+            int[] frequencies = mCpuScalingPolicies.getFrequencies(policy);
+            double[] powerByStep = new double[frequencies.length];
+            for (int step = 0; step < frequencies.length; step++) {
+                powerByStep[step] = mPowerProfile.getAveragePowerForCpuScalingStep(policy, step);
+            }
+
+            int[] policyStepToBracketMap = new int[frequencies.length];
+            mapScalingStepsToDefaultBrackets(policyStepToBracketMap, powerByStep,
+                    defaultBracketCountPerCluster);
+            int maxBracket = 0;
+            for (int step = 0; step < frequencies.length; step++) {
+                int bracket = bracketBase + policyStepToBracketMap[step];
+                stepToBracketMap[index++] = bracket;
+                if (bracket > maxBracket) {
+                    maxBracket = bracket;
+                }
+            }
+            bracketBase = maxBracket + 1;
+        }
+        return stepToBracketMap;
+    }
+
+    private int[] initDefaultPowerBrackets(int defaultCpuPowerBracketCount) {
+        int[] stepToBracketMap = new int[mCpuScalingPolicies.getScalingStepCount()];
+        double[] powerByStep = new double[mCpuScalingPolicies.getScalingStepCount()];
+        int index = 0;
+        int[] policies = mCpuScalingPolicies.getPolicies();
+        for (int policy : policies) {
+            int[] frequencies = mCpuScalingPolicies.getFrequencies(policy);
+            for (int step = 0; step < frequencies.length; step++) {
+                powerByStep[index++] = mPowerProfile.getAveragePowerForCpuScalingStep(policy, step);
+            }
+        }
+        mapScalingStepsToDefaultBrackets(stepToBracketMap, powerByStep,
+                defaultCpuPowerBracketCount);
+        return stepToBracketMap;
+    }
+
+    private static void mapScalingStepsToDefaultBrackets(int[] stepToBracketMap,
+            double[] powerByStep, int defaultCpuPowerBracketCount) {
+        double minPower = Double.MAX_VALUE;
+        double maxPower = Double.MIN_VALUE;
+        for (final double power : powerByStep) {
+            if (power < minPower) {
+                minPower = power;
+            }
+            if (power > maxPower) {
+                maxPower = power;
+            }
+        }
+        if (powerByStep.length <= defaultCpuPowerBracketCount) {
+            for (int index = 0; index < stepToBracketMap.length; index++) {
+                stepToBracketMap[index] = index;
+            }
+        } else {
+            final double minLogPower = Math.log(minPower);
+            final double logBracket = (Math.log(maxPower) - minLogPower)
+                                      / defaultCpuPowerBracketCount;
+
+            for (int step = 0; step < powerByStep.length; step++) {
+                int bracket = (int) ((Math.log(powerByStep[step]) - minLogPower) / logBracket);
+                if (bracket >= defaultCpuPowerBracketCount) {
+                    bracket = defaultCpuPowerBracketCount - 1;
+                }
+                stepToBracketMap[step] = bracket;
+            }
+        }
+    }
+
+    /**
+     * Prints the definitions of power brackets.
+     */
+    public void dumpCpuPowerBracketsLocked(PrintWriter pw) {
+        ensureInitialized();
+
+        pw.println("CPU power brackets; cluster/freq in MHz(avg current in mA):");
+        for (int bracket = 0; bracket < mLayout.getCpuPowerBracketCount(); bracket++) {
+            pw.print("    ");
+            pw.print(bracket);
+            pw.print(": ");
+            pw.println(getCpuPowerBracketDescription(bracket));
+        }
+    }
+
+    /**
+     * Description of a CPU power bracket: which cluster/frequency combinations are included.
+     */
+    @VisibleForTesting
+    public String getCpuPowerBracketDescription(int powerBracket) {
+        ensureInitialized();
+
+        int[] stepToPowerBracketMap = mLayout.getScalingStepToPowerBracketMap();
+        StringBuilder sb = new StringBuilder();
+        int index = 0;
+        int[] policies = mCpuScalingPolicies.getPolicies();
+        for (int policy : policies) {
+            int[] freqs = mCpuScalingPolicies.getFrequencies(policy);
+            for (int step = 0; step < freqs.length; step++) {
+                if (stepToPowerBracketMap[index] != powerBracket) {
+                    index++;
+                    continue;
+                }
+
+                if (sb.length() != 0) {
+                    sb.append(", ");
+                }
+                if (policies.length > 1) {
+                    sb.append(policy).append('/');
+                }
+                sb.append(freqs[step] / 1000);
+                sb.append('(');
+                sb.append(String.format(Locale.US, "%.1f",
+                        mPowerProfile.getAveragePowerForCpuScalingStep(policy, step)));
+                sb.append(')');
+
+                index++;
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Returns the descriptor of PowerStats produced by this collector.
+     */
+    @VisibleForTesting
+    public PowerStats.Descriptor getPowerStatsDescriptor() {
+        ensureInitialized();
+
+        return mPowerStatsDescriptor;
+    }
+
     @Override
     protected PowerStats collectStats() {
+        ensureInitialized();
+
+        if (!mIsPerUidTimeInStateSupported) {
+            return null;
+        }
+
         mCpuPowerStats.uidStats.clear();
-        long newTimestampNanos = mKernelCpuStatsReader.nativeReadCpuStats(
-                this::processUidStats, mScalingStepToPowerBracketMap, mLastUpdateTimestampNanos,
-                mTempUidStats);
+        // TODO(b/305120724): additionally retrieve time-in-cluster for each CPU cluster
+        long newTimestampNanos = mKernelCpuStatsReader.nativeReadCpuStats(this::processUidStats,
+                mLayout.getScalingStepToPowerBracketMap(), mLastUpdateTimestampNanos,
+                mTempCpuTimeByScalingStep, mTempUidStats);
+        for (int step = mLayout.getCpuScalingStepCount() - 1; step >= 0; step--) {
+            mLayout.setTimeByScalingStep(mCpuPowerStats.stats, step,
+                    mTempCpuTimeByScalingStep[step] - mCpuTimeByScalingStep[step]);
+            mCpuTimeByScalingStep[step] = mTempCpuTimeByScalingStep[step];
+        }
+
         mCpuPowerStats.durationMs =
                 (newTimestampNanos - mLastUpdateTimestampNanos) / NANOS_PER_MILLIS;
         mLastUpdateTimestampNanos = newTimestampNanos;
+
+        long uptimeMillis = mClock.uptimeMillis();
+        long uptimeDelta = uptimeMillis - mLastUpdateUptimeMillis;
+        mLastUpdateUptimeMillis = uptimeMillis;
+
+        if (uptimeDelta > mCpuPowerStats.durationMs) {
+            uptimeDelta = mCpuPowerStats.durationMs;
+        }
+        mLayout.setUptime(mCpuPowerStats.stats, uptimeDelta);
+
+        if (mCpuEnergyConsumerIds.length != 0) {
+            collectEnergyConsumers();
+        }
+
         return mCpuPowerStats;
     }
 
+    private void collectEnergyConsumers() {
+        int voltageMv = mVoltageSupplier.getAsInt();
+        if (voltageMv <= 0) {
+            Slog.wtf(TAG, "Unexpected battery voltage (" + voltageMv
+                          + " mV) when querying energy consumers");
+            return;
+        }
+
+        int averageVoltage = mLastVoltageMv != 0 ? (mLastVoltageMv + voltageMv) / 2 : voltageMv;
+        mLastVoltageMv = voltageMv;
+
+        CompletableFuture<EnergyConsumerResult[]> future =
+                mPowerStatsInternal.getEnergyConsumedAsync(mCpuEnergyConsumerIds);
+        EnergyConsumerResult[] results = null;
+        try {
+            results = future.get(
+                    POWER_STATS_ENERGY_CONSUMERS_TIMEOUT, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException | ExecutionException | TimeoutException e) {
+            Slog.e(TAG, "Could not obtain energy consumers from PowerStatsService", e);
+        }
+        if (results == null) {
+            return;
+        }
+
+        for (int i = 0; i < mCpuEnergyConsumerIds.length; i++) {
+            int id = mCpuEnergyConsumerIds[i];
+            for (EnergyConsumerResult result : results) {
+                if (result.id == id) {
+                    long energyDelta = mLastConsumedEnergyUws[i] != ENERGY_UNSPECIFIED
+                            ? result.energyUWs - mLastConsumedEnergyUws[i] : 0;
+                    if (energyDelta < 0) {
+                        // Likely, restart of powerstats HAL
+                        energyDelta = 0;
+                    }
+                    mLayout.setConsumedEnergy(mCpuPowerStats.stats, i,
+                            uJtoUc(energyDelta, averageVoltage));
+                    mLastConsumedEnergyUws[i] = result.energyUWs;
+                    break;
+                }
+            }
+        }
+    }
+
     @VisibleForNative
     interface KernelCpuStatsCallback {
         @Keep // Called from native
-        void processUidStats(int uid, long[] stats);
+        void processUidStats(int uid, long[] timeByPowerBracket);
     }
 
-    private void processUidStats(int uid, long[] stats) {
+    private void processUidStats(int uid, long[] timeByPowerBracket) {
+        int powerBracketCount = mLayout.getCpuPowerBracketCount();
+
         UidStats uidStats = mUidStats.get(uid);
         if (uidStats == null) {
             uidStats = new UidStats();
-            uidStats.stats = new long[mUidStatsSize];
-            uidStats.delta = new long[mUidStatsSize];
+            uidStats.timeByPowerBracket = new long[powerBracketCount];
+            uidStats.stats = new long[mLayout.getUidStatsArrayLength()];
             mUidStats.put(uid, uidStats);
         }
 
         boolean nonzero = false;
-        for (int i = mUidStatsSize - 1; i >= 0; i--) {
-            long delta = uidStats.delta[i] = stats[i] - uidStats.stats[i];
+        for (int bracket = powerBracketCount - 1; bracket >= 0; bracket--) {
+            long delta = timeByPowerBracket[bracket] - uidStats.timeByPowerBracket[bracket];
             if (delta != 0) {
                 nonzero = true;
             }
-            uidStats.stats[i] = stats[i];
+            mLayout.setUidTimeByPowerBracket(uidStats.stats, bracket, delta);
+            uidStats.timeByPowerBracket[bracket] = timeByPowerBracket[bracket];
         }
         if (nonzero) {
-            mCpuPowerStats.uidStats.put(uid, uidStats.delta);
+            mCpuPowerStats.uidStats.put(uid, uidStats.stats);
         }
     }
 
@@ -128,13 +765,15 @@
      * Native class that retrieves CPU stats from the kernel.
      */
     public static class KernelCpuStatsReader {
+        protected native boolean nativeIsSupportedFeature();
+
         protected native long nativeReadCpuStats(KernelCpuStatsCallback callback,
                 int[] scalingStepToPowerBracketMap, long lastUpdateTimestampNanos,
-                long[] tempForUidStats);
+                long[] outCpuTimeByScalingStep, long[] tempForUidStats);
     }
 
     private static class UidStats {
         public long[] stats;
-        public long[] delta;
+        public long[] timeByPowerBracket;
     }
 }
diff --git a/services/core/java/com/android/server/power/stats/PowerComponentAggregatedPowerStats.java b/services/core/java/com/android/server/power/stats/PowerComponentAggregatedPowerStats.java
index 05c0a13..2c7843e 100644
--- a/services/core/java/com/android/server/power/stats/PowerComponentAggregatedPowerStats.java
+++ b/services/core/java/com/android/server/power/stats/PowerComponentAggregatedPowerStats.java
@@ -16,6 +16,8 @@
 
 package com.android.server.power.stats;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.util.IndentingPrintWriter;
 import android.util.SparseArray;
 
@@ -46,6 +48,8 @@
     public final int powerComponentId;
     private final MultiStateStats.States[] mDeviceStateConfig;
     private final MultiStateStats.States[] mUidStateConfig;
+    @NonNull
+    private final AggregatedPowerStatsConfig.PowerComponent mConfig;
     private final int[] mDeviceStates;
     private final long[] mDeviceStateTimestamps;
 
@@ -62,13 +66,20 @@
     }
 
     PowerComponentAggregatedPowerStats(AggregatedPowerStatsConfig.PowerComponent config) {
-        this.powerComponentId = config.getPowerComponentId();
+        mConfig = config;
+        powerComponentId = config.getPowerComponentId();
         mDeviceStateConfig = config.getDeviceStateConfig();
         mUidStateConfig = config.getUidStateConfig();
         mDeviceStates = new int[mDeviceStateConfig.length];
         mDeviceStateTimestamps = new long[mDeviceStateConfig.length];
     }
 
+    @NonNull
+    public AggregatedPowerStatsConfig.PowerComponent getConfig() {
+        return mConfig;
+    }
+
+    @Nullable
     public PowerStats.Descriptor getPowerStatsDescriptor() {
         return mPowerStatsDescriptor;
     }
@@ -108,6 +119,16 @@
         }
     }
 
+    void setDeviceStats(@AggregatedPowerStatsConfig.TrackedState int[] states, long[] values) {
+        mDeviceStats.setStats(states, values);
+    }
+
+    void setUidStats(int uid, @AggregatedPowerStatsConfig.TrackedState int[] states,
+            long[] values) {
+        UidStats uidStats = getUidStats(uid);
+        uidStats.stats.setStats(states, values);
+    }
+
     boolean isCompatible(PowerStats powerStats) {
         return mPowerStatsDescriptor == null || mPowerStatsDescriptor.equals(powerStats.descriptor);
     }
@@ -298,7 +319,8 @@
         if (mDeviceStats != null) {
             ipw.println(mPowerStatsDescriptor.name);
             ipw.increaseIndent();
-            mDeviceStats.dump(ipw);
+            mDeviceStats.dump(ipw, stats ->
+                    mConfig.getProcessor().deviceStatsToString(mPowerStatsDescriptor, stats));
             ipw.decreaseIndent();
         }
     }
@@ -308,7 +330,8 @@
         if (uidStats != null && uidStats.stats != null) {
             ipw.println(mPowerStatsDescriptor.name);
             ipw.increaseIndent();
-            uidStats.stats.dump(ipw);
+            uidStats.stats.dump(ipw, stats ->
+                    mConfig.getProcessor().uidStatsToString(mPowerStatsDescriptor, stats));
             ipw.decreaseIndent();
         }
     }
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsAggregator.java b/services/core/java/com/android/server/power/stats/PowerStatsAggregator.java
index f374fb7..2f9d567 100644
--- a/services/core/java/com/android/server/power/stats/PowerStatsAggregator.java
+++ b/services/core/java/com/android/server/power/stats/PowerStatsAggregator.java
@@ -16,6 +16,7 @@
 package com.android.server.power.stats;
 
 import android.os.BatteryStats;
+import android.util.SparseArray;
 
 import com.android.internal.os.BatteryStatsHistory;
 import com.android.internal.os.BatteryStatsHistoryIterator;
@@ -30,11 +31,17 @@
 public class PowerStatsAggregator {
     private final AggregatedPowerStats mStats;
     private final BatteryStatsHistory mHistory;
+    private final SparseArray<AggregatedPowerStatsProcessor> mProcessors = new SparseArray<>();
 
     public PowerStatsAggregator(AggregatedPowerStatsConfig aggregatedPowerStatsConfig,
             BatteryStatsHistory history) {
         mStats = new AggregatedPowerStats(aggregatedPowerStatsConfig);
         mHistory = history;
+        for (AggregatedPowerStatsConfig.PowerComponent powerComponentsConfig :
+                aggregatedPowerStatsConfig.getPowerComponentsAggregatedStatsConfigs()) {
+            AggregatedPowerStatsProcessor processor = powerComponentsConfig.getProcessor();
+            mProcessors.put(powerComponentsConfig.getPowerComponentId(), processor);
+        }
     }
 
     /**
@@ -100,6 +107,7 @@
                     if (!mStats.isCompatible(item.powerStats)) {
                         if (lastTime > baseTime) {
                             mStats.setDuration(lastTime - baseTime);
+                            finish(mStats);
                             consumer.accept(mStats);
                         }
                         mStats.reset();
@@ -112,9 +120,20 @@
         }
         if (lastTime > baseTime) {
             mStats.setDuration(lastTime - baseTime);
+            finish(mStats);
             consumer.accept(mStats);
         }
 
         mStats.reset();     // to free up memory
     }
+
+    private void finish(AggregatedPowerStats stats) {
+        for (int i = 0; i < mProcessors.size(); i++) {
+            PowerComponentAggregatedPowerStats component =
+                    stats.getPowerComponentStats(mProcessors.keyAt(i));
+            if (component != null) {
+                mProcessors.valueAt(i).finish(component);
+            }
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsCollector.java b/services/core/java/com/android/server/power/stats/PowerStatsCollector.java
index b49c89f..84cc21e 100644
--- a/services/core/java/com/android/server/power/stats/PowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/PowerStatsCollector.java
@@ -38,8 +38,9 @@
  * except where noted.
  */
 public abstract class PowerStatsCollector {
+    private static final int MILLIVOLTS_PER_VOLT = 1000;
     private final Handler mHandler;
-    private final Clock mClock;
+    protected final Clock mClock;
     private final long mThrottlePeriodMs;
     private final Runnable mCollectAndDeliverStats = this::collectAndDeliverStats;
     private boolean mEnabled;
@@ -100,6 +101,9 @@
     @SuppressWarnings("GuardedBy")  // Field is volatile
     private void collectAndDeliverStats() {
         PowerStats stats = collectStats();
+        if (stats == null) {
+            return;
+        }
         for (Consumer<PowerStats> consumer : mConsumerList) {
             consumer.accept(stats);
         }
@@ -180,4 +184,11 @@
         mHandler.post(done::open);
         done.block();
     }
+
+    /** Calculate charge consumption (in microcoulombs) from a given energy and voltage */
+    protected long uJtoUc(long deltaEnergyUj, int avgVoltageMv) {
+        // To overflow, a 3.7V 10000mAh battery would need to completely drain 69244 times
+        // since the last snapshot. Round off to the nearest whole long.
+        return (deltaEnergyUj * MILLIVOLTS_PER_VOLT + (avgVoltageMv / 2)) / avgVoltageMv;
+    }
 }
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsScheduler.java b/services/core/java/com/android/server/power/stats/PowerStatsScheduler.java
index 58619c7..551302e 100644
--- a/services/core/java/com/android/server/power/stats/PowerStatsScheduler.java
+++ b/services/core/java/com/android/server/power/stats/PowerStatsScheduler.java
@@ -108,6 +108,9 @@
         long currentTimeMillis = mClock.currentTimeMillis();
         long currentMonotonicTime = mMonotonicClock.monotonicTime();
         long startTime = getLastSavedSpanEndMonotonicTime();
+        if (startTime < 0) {
+            startTime = mBatteryStats.getHistory().getStartTime();
+        }
         long endTimeMs = alignToWallClock(startTime + mAggregatedPowerStatsSpanDuration,
                 mAggregatedPowerStatsSpanDuration, currentMonotonicTime, currentTimeMillis);
         while (endTimeMs <= currentMonotonicTime) {
@@ -214,6 +217,7 @@
             return mLastSavedSpanEndMonotonicTime;
         }
 
+        mLastSavedSpanEndMonotonicTime = -1;
         for (PowerStatsSpan.Metadata metadata : mPowerStatsStore.getTableOfContents()) {
             if (metadata.getSections().contains(AggregatedPowerStatsSection.TYPE)) {
                 for (PowerStatsSpan.TimeFrame timeFrame : metadata.getTimeFrames()) {
diff --git a/services/core/java/com/android/server/rollback/AppDataRollbackHelper.java b/services/core/java/com/android/server/rollback/AppDataRollbackHelper.java
index 7b0fe9a..a01bac6 100644
--- a/services/core/java/com/android/server/rollback/AppDataRollbackHelper.java
+++ b/services/core/java/com/android/server/rollback/AppDataRollbackHelper.java
@@ -266,10 +266,10 @@
     }
 
     /**
-     * @return {@code true} iff. {@code userId} is locked on an FBE device.
+     * @return {@code true} iff the credential-encrypted storage for {@code userId} is locked.
      */
     @VisibleForTesting
     public boolean isUserCredentialLocked(int userId) {
-        return StorageManager.isFileEncrypted() && !StorageManager.isUserKeyUnlocked(userId);
+        return StorageManager.isFileEncrypted() && !StorageManager.isCeStorageUnlocked(userId);
     }
 }
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index fd8f6bf..bdab4d4 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -9317,8 +9317,9 @@
         final Task rootTask = getRootTask();
         final float minAspectRatio = getMinAspectRatio();
         final TaskFragment organizedTf = getOrganizedTaskFragment();
+        float aspectRatioToApply = desiredAspectRatio;
         if (task == null || rootTask == null
-                || (maxAspectRatio < 1 && minAspectRatio < 1 && desiredAspectRatio < 1)
+                || (maxAspectRatio < 1 && minAspectRatio < 1 && aspectRatioToApply < 1)
                 // Don't set aspect ratio if we are in VR mode.
                 || isInVrUiMode(getConfiguration())
                 // TODO(b/232898850): Always respect aspect ratio requests.
@@ -9331,30 +9332,30 @@
         final int containingAppHeight = containingAppBounds.height();
         final float containingRatio = computeAspectRatio(containingAppBounds);
 
-        if (desiredAspectRatio < 1) {
-            desiredAspectRatio = containingRatio;
+        if (aspectRatioToApply < 1) {
+            aspectRatioToApply = containingRatio;
         }
 
-        if (maxAspectRatio >= 1 && desiredAspectRatio > maxAspectRatio) {
-            desiredAspectRatio = maxAspectRatio;
-        } else if (minAspectRatio >= 1 && desiredAspectRatio < minAspectRatio) {
-            desiredAspectRatio = minAspectRatio;
+        if (maxAspectRatio >= 1 && aspectRatioToApply > maxAspectRatio) {
+            aspectRatioToApply = maxAspectRatio;
+        } else if (minAspectRatio >= 1 && aspectRatioToApply < minAspectRatio) {
+            aspectRatioToApply = minAspectRatio;
         }
 
         int activityWidth = containingAppWidth;
         int activityHeight = containingAppHeight;
 
-        if (containingRatio - desiredAspectRatio > ASPECT_RATIO_ROUNDING_TOLERANCE) {
+        if (containingRatio - aspectRatioToApply > ASPECT_RATIO_ROUNDING_TOLERANCE) {
             if (containingAppWidth < containingAppHeight) {
                 // Width is the shorter side, so we use that to figure-out what the max. height
                 // should be given the aspect ratio.
-                activityHeight = (int) ((activityWidth * desiredAspectRatio) + 0.5f);
+                activityHeight = (int) ((activityWidth * aspectRatioToApply) + 0.5f);
             } else {
                 // Height is the shorter side, so we use that to figure-out what the max. width
                 // should be given the aspect ratio.
-                activityWidth = (int) ((activityHeight * desiredAspectRatio) + 0.5f);
+                activityWidth = (int) ((activityHeight * aspectRatioToApply) + 0.5f);
             }
-        } else if (desiredAspectRatio - containingRatio > ASPECT_RATIO_ROUNDING_TOLERANCE) {
+        } else if (aspectRatioToApply - containingRatio > ASPECT_RATIO_ROUNDING_TOLERANCE) {
             boolean adjustWidth;
             switch (getRequestedConfigurationOrientation()) {
                 case ORIENTATION_LANDSCAPE:
@@ -9382,9 +9383,9 @@
                     break;
             }
             if (adjustWidth) {
-                activityWidth = (int) ((activityHeight / desiredAspectRatio) + 0.5f);
+                activityWidth = (int) ((activityHeight / aspectRatioToApply) + 0.5f);
             } else {
-                activityHeight = (int) ((activityWidth / desiredAspectRatio) + 0.5f);
+                activityHeight = (int) ((activityWidth / aspectRatioToApply) + 0.5f);
             }
         }
 
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 7cccf6b..34bf8ed 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -1097,23 +1097,22 @@
                         "shouldAbortBackgroundActivityStart");
                 BackgroundActivityStartController balController =
                         mSupervisor.getBackgroundActivityLaunchController();
-                balCode =
+                BackgroundActivityStartController.BalVerdict balVerdict =
                         balController.checkBackgroundActivityStart(
-                                callingUid,
-                                callingPid,
-                                callingPackage,
-                                realCallingUid,
-                                realCallingPid,
-                                callerApp,
-                                request.originatingPendingIntent,
-                                request.backgroundStartPrivileges,
-                                intent,
-                                checkedOptions);
-                if (balCode != BAL_ALLOW_DEFAULT) {
-                    request.logMessage.append(" (").append(
-                                    BackgroundActivityStartController.balCodeToString(balCode))
-                            .append(")");
-                }
+                            callingUid,
+                            callingPid,
+                            callingPackage,
+                            realCallingUid,
+                            realCallingPid,
+                            callerApp,
+                            request.originatingPendingIntent,
+                            request.backgroundStartPrivileges,
+                            intent,
+                            checkedOptions);
+                balCode = balVerdict.getCode();
+                request.logMessage.append(" (").append(
+                                BackgroundActivityStartController.balCodeToString(balCode))
+                        .append(")");
             } finally {
                 Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
             }
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index e97bda2..9b7b8de 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -22,7 +22,6 @@
 import static android.os.Process.SYSTEM_UID;
 import static android.provider.DeviceConfig.NAMESPACE_WINDOW_MANAGER;
 
-import static com.android.internal.util.Preconditions.checkState;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_ACTIVITY_STARTS;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
@@ -40,7 +39,6 @@
 import android.app.ActivityOptions;
 import android.app.AppOpsManager;
 import android.app.BackgroundStartPrivileges;
-import android.app.ComponentOptions;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.PackageManager;
@@ -73,14 +71,18 @@
 
     private static final String TAG =
             TAG_WITH_CLASS_NAME ? "BackgroundActivityStartController" : TAG_ATM;
-    public static final String VERDICT_ALLOWED = "Activity start allowed";
-    public static final String VERDICT_WOULD_BE_ALLOWED_IF_SENDER_GRANTS_BAL =
-            "Activity start would be allowed if the sender granted BAL privileges";
 
     private static final long ASM_GRACEPERIOD_TIMEOUT_MS = TIMEOUT_MS;
     private static final int ASM_GRACEPERIOD_MAX_REPEATS = 5;
+    public static final ActivityOptions ACTIVITY_OPTIONS_SYSTEM_DEFINED =
+            ActivityOptions.makeBasic()
+                    .setPendingIntentBackgroundActivityStartMode(
+                            ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED)
+                    .setPendingIntentCreatorBackgroundActivityStartMode(
+                            ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED);
 
     private final ActivityTaskManagerService mService;
+
     private final ActivityTaskSupervisor mSupervisor;
 
     // TODO(b/263368846) Rename when ASM logic is moved in
@@ -202,15 +204,181 @@
         return checkBackgroundActivityStart(callingUid, callingPid, callingPackage,
                 realCallingUid, realCallingPid,
                 callerApp, originatingPendingIntent,
-                backgroundStartPrivileges, intent, checkedOptions) == BAL_BLOCK;
+                backgroundStartPrivileges, intent, checkedOptions).blocks();
+    }
+
+    private class BalState {
+
+        private final String mCallingPackage;
+        private final int mCallingUid;
+        private final int mCallingPid;
+        private final @ActivityTaskManagerService.AppSwitchState int mAppSwitchState;
+        private final boolean mCallingUidHasAnyVisibleWindow;
+        private final @ActivityManager.ProcessState int mCallingUidProcState;
+        private final boolean mIsCallingUidPersistentSystemProcess;
+        private final BackgroundStartPrivileges mBalAllowedByPiSender;
+        private final BackgroundStartPrivileges mBalAllowedByPiCreator;
+        private final String mRealCallingPackage;
+        private final int mRealCallingUid;
+        private final int mRealCallingPid;
+        private final boolean mRealCallingUidHasAnyVisibleWindow;
+        private final @ActivityManager.ProcessState int mRealCallingUidProcState;
+        private final boolean mIsRealCallingUidPersistentSystemProcess;
+        private final PendingIntentRecord mOriginatingPendingIntent;
+        private final BackgroundStartPrivileges mBackgroundStartPrivileges;
+        private final Intent mIntent;
+        private final WindowProcessController mCallerApp;
+        private final WindowProcessController mRealCallerApp;
+
+        private BalState(int callingUid, int callingPid, final String callingPackage,
+                 int realCallingUid, int realCallingPid,
+                 WindowProcessController callerApp,
+                 PendingIntentRecord originatingPendingIntent,
+                 BackgroundStartPrivileges backgroundStartPrivileges,
+                 Intent intent,
+                 ActivityOptions checkedOptions) {
+            this.mCallingPackage = callingPackage;
+            mCallingUid = callingUid;
+            mCallingPid = callingPid;
+            mRealCallingUid = realCallingUid;
+            mRealCallingPid = realCallingPid;
+            mCallerApp = callerApp;
+            mBackgroundStartPrivileges = backgroundStartPrivileges;
+            mOriginatingPendingIntent = originatingPendingIntent;
+            mIntent = intent;
+            mRealCallingPackage = mService.getPackageNameIfUnique(realCallingUid, realCallingPid);
+            mBalAllowedByPiSender =
+                    PendingIntentRecord.getBackgroundStartPrivilegesAllowedByCaller(checkedOptions,
+                            realCallingUid, mRealCallingPackage);
+            mBalAllowedByPiCreator =
+                    checkedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
+                            == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED
+                            ? BackgroundStartPrivileges.NONE : BackgroundStartPrivileges.ALLOW_BAL;
+            mAppSwitchState = mService.getBalAppSwitchesState();
+            mCallingUidProcState = mService.mActiveUids.getUidState(callingUid);
+            mIsCallingUidPersistentSystemProcess =
+                    mCallingUidProcState <= ActivityManager.PROCESS_STATE_PERSISTENT_UI;
+            mCallingUidHasAnyVisibleWindow = mService.hasActiveVisibleWindow(callingUid);
+            if (callingUid == realCallingUid) {
+                mRealCallingUidProcState = mCallingUidProcState;
+                mRealCallingUidHasAnyVisibleWindow = mCallingUidHasAnyVisibleWindow;
+                // In the PendingIntent case callerApp is not passed in, so resolve it ourselves.
+                mRealCallerApp = callerApp == null
+                        ? mService.getProcessController(realCallingPid, realCallingUid)
+                        : callerApp;
+                mIsRealCallingUidPersistentSystemProcess = mIsCallingUidPersistentSystemProcess;
+            } else {
+                mRealCallingUidProcState = mService.mActiveUids.getUidState(realCallingUid);
+                mRealCallingUidHasAnyVisibleWindow =
+                        mService.hasActiveVisibleWindow(realCallingUid);
+                mRealCallerApp = mService.getProcessController(realCallingPid, realCallingUid);
+                mIsRealCallingUidPersistentSystemProcess =
+                        mRealCallingUidProcState <= ActivityManager.PROCESS_STATE_PERSISTENT_UI;
+            }
+        }
+
+        private String getDebugPackageName(String packageName, int uid) {
+            if (packageName != null) {
+                return packageName; // use actual package
+            }
+            if (uid == 0) {
+                return "root[debugOnly]";
+            }
+            String name = mService.mContext.getPackageManager().getNameForUid(uid);
+            if (name == null) {
+                name = "uid=" + uid;
+            }
+            return name + "[debugOnly]";
+        }
+
+        private String dump(BalVerdict resultIfPiCreatorAllowsBal,
+                           BalVerdict resultIfPiSenderAllowsBal) {
+            return " [callingPackage: " + getDebugPackageName(mCallingPackage, mCallingUid)
+                    + "; callingUid: " + mCallingUid
+                    + "; appSwitchState: " + mAppSwitchState
+                    + "; callingUidHasAnyVisibleWindow: " + mCallingUidHasAnyVisibleWindow
+                    + "; callingUidProcState: " + DebugUtils.valueToString(
+                        ActivityManager.class, "PROCESS_STATE_", mCallingUidProcState)
+                    + "; isCallingUidPersistentSystemProcess: "
+                        + mIsCallingUidPersistentSystemProcess
+                    + "; balAllowedByPiCreator: " + mBalAllowedByPiCreator
+                    + "; balAllowedByPiSender: " + mBalAllowedByPiSender
+                    + "; realCallingPackage: "
+                        + getDebugPackageName(mRealCallingPackage, mRealCallingUid)
+                    + "; realCallingUid: " + mRealCallingUid
+                    + "; realCallingPid: " + mRealCallingPid
+                    + "; realCallingUidHasAnyVisibleWindow: " + mRealCallingUidHasAnyVisibleWindow
+                    + "; realCallingUidProcState: " + DebugUtils.valueToString(
+                        ActivityManager.class, "PROCESS_STATE_", mRealCallingUidProcState)
+                    + "; isRealCallingUidPersistentSystemProcess: "
+                        + mIsRealCallingUidPersistentSystemProcess
+                    + "; originatingPendingIntent: " + mOriginatingPendingIntent
+                    + "; backgroundStartPrivileges: " + mBackgroundStartPrivileges
+                    + "; intent: " + mIntent
+                    + "; callerApp: " + mCallerApp
+                    + "; realCallerApp: " + mRealCallerApp
+                    + "; inVisibleTask: "
+                        + (mCallerApp != null && mCallerApp.hasActivityInVisibleTask())
+                    + "; realInVisibleTask: "
+                        + (mRealCallerApp != null && mRealCallerApp.hasActivityInVisibleTask())
+                    + "; resultIfPiSenderAllowsBal: " + resultIfPiSenderAllowsBal
+                    + "; resultIfPiCreatorAllowsBal: " + resultIfPiCreatorAllowsBal
+                    + "]";
+        }
+    }
+
+    static class BalVerdict {
+
+        static final BalVerdict BLOCK = new BalVerdict(BAL_BLOCK, false, "Blocked");
+        private final @BalCode int mCode;
+        private final boolean mBackground;
+        private final String mMessage;
+        private String mProcessInfo;
+
+        BalVerdict(@BalCode int balCode, boolean background, String message) {
+            this.mBackground = background;
+            this.mCode = balCode;
+            this.mMessage = message;
+        }
+
+        public BalVerdict withProcessInfo(String msg, WindowProcessController process) {
+            mProcessInfo = msg + " (uid=" + process.mUid + ",pid=" + process.getPid() + ")";
+            return this;
+        }
+
+        boolean blocks() {
+            return mCode == BAL_BLOCK;
+        }
+
+        boolean allows() {
+            return !blocks();
+        }
+
+        public String toString() {
+            StringBuilder builder = new StringBuilder();
+            if (mBackground) {
+                builder.append("Background ");
+            }
+            builder.append("Activity start allowed: " + mMessage + ".");
+            builder.append("BAL Code: ");
+            builder.append(balCodeToString(mCode));
+            if (mProcessInfo != null) {
+                builder.append(" ");
+                builder.append(mProcessInfo);
+            }
+            return builder.toString();
+        }
+
+        public @BalCode int getCode() {
+            return mCode;
+        }
     }
 
     /**
      * @return A code denoting which BAL rule allows an activity to be started,
-     * or {@link BAL_BLOCK} if the launch should be blocked
+     * or {@link #BAL_BLOCK} if the launch should be blocked
      */
-    @BalCode
-    int checkBackgroundActivityStart(
+    BalVerdict checkBackgroundActivityStart(
             int callingUid,
             int callingPid,
             final String callingPackage,
@@ -221,36 +389,148 @@
             BackgroundStartPrivileges backgroundStartPrivileges,
             Intent intent,
             ActivityOptions checkedOptions) {
+
+        if (checkedOptions == null) {
+            // replace null with a constant to simplify evaluation
+            checkedOptions = ACTIVITY_OPTIONS_SYSTEM_DEFINED;
+        }
+
+        BalState state = new BalState(callingUid, callingPid, callingPackage,
+                realCallingUid, realCallingPid, callerApp, originatingPendingIntent,
+                backgroundStartPrivileges, intent, checkedOptions);
+
+        // In the case of an SDK sandbox calling uid, check if the corresponding app uid has a
+        // visible window.
+        if (Process.isSdkSandboxUid(state.mRealCallingUid)) {
+            int realCallingSdkSandboxUidToAppUid =
+                    Process.getAppUidForSdkSandboxUid(state.mRealCallingUid);
+            // realCallingSdkSandboxUidToAppUid should probably just be used instead (or in addition
+            // to realCallingUid when calculating resultForRealCaller below.
+            if (mService.hasActiveVisibleWindow(realCallingSdkSandboxUidToAppUid)) {
+                BalVerdict balVerdict = new BalVerdict(BAL_ALLOW_SDK_SANDBOX, /*background*/ false,
+                        "uid in SDK sandbox has visible (non-toast) window");
+                return statsLog(balVerdict, state);
+            }
+        }
+
+        BalVerdict resultForCaller = checkBackgroundActivityStartAllowedByCaller(state);
+        BalVerdict resultForRealCaller = callingUid == realCallingUid
+                ? resultForCaller // no need to calculate again
+                : checkBackgroundActivityStartAllowedBySender(state, checkedOptions);
+
+        if (resultForCaller.allows()
+                && checkedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
+                == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) {
+            if (DEBUG_ACTIVITY_STARTS) {
+                Slog.d(TAG, "Activity start explicitly allowed by PI creator. "
+                        + state.dump(resultForCaller, resultForRealCaller));
+            }
+            return statsLog(resultForCaller, state);
+        }
+        if (resultForRealCaller.allows()
+                && checkedOptions.getPendingIntentBackgroundActivityStartMode()
+                == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) {
+            if (DEBUG_ACTIVITY_STARTS) {
+                Slog.d(TAG, "Activity start explicitly allowed by PI sender. "
+                        + state.dump(resultForCaller, resultForRealCaller));
+            }
+            return statsLog(resultForRealCaller, state);
+        }
+        if (resultForCaller.allows() && resultForRealCaller.allows()
+                && checkedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
+                == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED
+                && checkedOptions.getPendingIntentBackgroundActivityStartMode()
+                == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED) {
+            // Both caller and real caller allow with system defined behavior
+            Slog.wtf(TAG,
+                    "With Android 15 BAL hardening this activity start would be blocked"
+                            + " (missing opt in by PI creator)! "
+                            + state.dump(resultForCaller, resultForRealCaller));
+            // return the realCaller result for backwards compatibility
+            return statsLog(resultForRealCaller, state);
+        }
+        if (resultForCaller.allows()
+                && checkedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
+                == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED) {
+            // Allowed before V by creator
+            Slog.wtf(TAG,
+                    "With Android 15 BAL hardening this activity start would be blocked"
+                            + " (missing opt in by PI creator)! "
+                            + state.dump(resultForCaller, resultForRealCaller));
+            return statsLog(resultForCaller, state);
+        }
+        if (resultForRealCaller.allows()
+                && checkedOptions.getPendingIntentBackgroundActivityStartMode()
+                == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED) {
+            // Allowed before U by sender
+            if (state.mBalAllowedByPiSender.allowsBackgroundActivityStarts()) {
+                Slog.wtf(TAG,
+                        "With Android 14 BAL hardening this activity start would be blocked"
+                                + " (missing opt in by PI sender)! "
+                                + state.dump(resultForCaller, resultForRealCaller));
+                return statsLog(resultForRealCaller, state);
+            }
+            Slog.wtf(TAG, "Without Android 14 BAL hardening this activity start would be allowed"
+                    + " (missing opt in by PI sender)! "
+                    + state.dump(resultForCaller, resultForRealCaller));
+            // fall through
+        }
+        // anything that has fallen through would currently be aborted
+        Slog.w(TAG, "Background activity launch blocked"
+                + state.dump(resultForCaller, resultForRealCaller));
+        // log aborted activity start to TRON
+        if (mService.isActivityStartsLoggingEnabled()) {
+            mSupervisor
+                    .getActivityMetricsLogger()
+                    .logAbortedBgActivityStart(
+                            intent,
+                            callerApp,
+                            callingUid,
+                            callingPackage,
+                            state.mCallingUidProcState,
+                            state.mCallingUidHasAnyVisibleWindow,
+                            realCallingUid,
+                            state.mRealCallingUidProcState,
+                            state.mRealCallingUidHasAnyVisibleWindow,
+                            (originatingPendingIntent != null));
+        }
+        return statsLog(BalVerdict.BLOCK, state);
+    }
+
+    /**
+     * @return A code denoting which BAL rule allows an activity to be started,
+     * or {@link #BAL_BLOCK} if the launch should be blocked
+     */
+    BalVerdict checkBackgroundActivityStartAllowedByCaller(BalState state) {
+        int callingUid = state.mCallingUid;
+        int callingPid = state.mCallingPid;
+        final String callingPackage = state.mCallingPackage;
+        WindowProcessController callerApp = state.mCallerApp;
+
         // don't abort for the most important UIDs
         final int callingAppId = UserHandle.getAppId(callingUid);
-        final boolean useCallingUidState =
-                originatingPendingIntent == null
-                        || checkedOptions == null
-                        || checkedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
-                                != ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED;
-        if (useCallingUidState) {
-            if (callingUid == Process.ROOT_UID
-                    || callingAppId == Process.SYSTEM_UID
-                    || callingAppId == Process.NFC_UID) {
-                return logStartAllowedAndReturnCode(BAL_ALLOW_ALLOWLISTED_UID, /*background*/ false,
-                        callingUid, realCallingUid, intent, "Important callingUid");
-            }
+        if (callingUid == Process.ROOT_UID
+                || callingAppId == Process.SYSTEM_UID
+                || callingAppId == Process.NFC_UID) {
+            return new BalVerdict(
+                    BAL_ALLOW_ALLOWLISTED_UID, /*background*/ false,
+                     "Important callingUid");
+        }
 
-            // Always allow home application to start activities.
-            if (isHomeApp(callingUid, callingPackage)) {
-                return logStartAllowedAndReturnCode(BAL_ALLOW_ALLOWLISTED_COMPONENT,
-                        /*background*/ false, callingUid, realCallingUid, intent,
-                        "Home app");
-            }
+        // Always allow home application to start activities.
+        if (isHomeApp(callingUid, callingPackage)) {
+            return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
+                    /*background*/ false,
+                    "Home app");
+        }
 
-            // IME should always be allowed to start activity, like IME settings.
-            final WindowState imeWindow =
-                    mService.mRootWindowContainer.getCurrentInputMethodWindow();
-            if (imeWindow != null && callingAppId == imeWindow.mOwnerUid) {
-                return logStartAllowedAndReturnCode(BAL_ALLOW_ALLOWLISTED_COMPONENT,
-                        /*background*/ false, callingUid, realCallingUid, intent,
-                        "Active ime");
-            }
+        // IME should always be allowed to start activity, like IME settings.
+        final WindowState imeWindow =
+                mService.mRootWindowContainer.getCurrentInputMethodWindow();
+        if (imeWindow != null && callingAppId == imeWindow.mOwnerUid) {
+            return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
+                    /*background*/ false,
+                    "Active ime");
         }
 
         // This is used to block background activity launch even if the app is still
@@ -269,331 +549,169 @@
                 appSwitchState == APP_SWITCH_ALLOW || appSwitchState == APP_SWITCH_FG_ONLY;
         final boolean allowCallingUidStartActivity =
                 ((appSwitchAllowedOrFg || mService.mActiveUids.hasNonAppVisibleWindow(callingUid))
-                                && callingUidHasAnyVisibleWindow)
+                        && callingUidHasAnyVisibleWindow)
                         || isCallingUidPersistentSystemProcess;
-        if (useCallingUidState && allowCallingUidStartActivity) {
-            return logStartAllowedAndReturnCode(BAL_ALLOW_VISIBLE_WINDOW,
-                    /*background*/ false, callingUid, realCallingUid, intent,
+        if (allowCallingUidStartActivity) {
+            return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW,
+                    /*background*/ false,
                     "callingUidHasAnyVisibleWindow = "
                             + callingUid
                             + ", isCallingUidPersistentSystemProcess = "
                             + isCallingUidPersistentSystemProcess);
         }
-        // take realCallingUid into consideration
-        final int realCallingUidProcState =
-                (callingUid == realCallingUid)
-                        ? callingUidProcState
-                        : mService.mActiveUids.getUidState(realCallingUid);
-        final boolean realCallingUidHasAnyVisibleWindow =
-                (callingUid == realCallingUid)
-                        ? callingUidHasAnyVisibleWindow
-                        : mService.hasActiveVisibleWindow(realCallingUid);
-        final int realCallingAppId = UserHandle.getAppId(realCallingUid);
-        final boolean isRealCallingUidPersistentSystemProcess =
-                (callingUid == realCallingUid)
-                        ? isCallingUidPersistentSystemProcess
-                        : (realCallingAppId == Process.SYSTEM_UID)
-                                || realCallingUidProcState
-                                        <= ActivityManager.PROCESS_STATE_PERSISTENT_UI;
 
-        // In the case of an SDK sandbox calling uid, check if the corresponding app uid has a
-        // visible window.
-        if (Process.isSdkSandboxUid(realCallingUid)) {
-            int realCallingSdkSandboxUidToAppUid =
-                    Process.getAppUidForSdkSandboxUid(realCallingUid);
-
-            if (mService.hasActiveVisibleWindow(realCallingSdkSandboxUidToAppUid)) {
-                return logStartAllowedAndReturnCode(BAL_ALLOW_SDK_SANDBOX,
-                        /*background*/ false, callingUid, realCallingUid, intent,
-                        "uid in SDK sandbox has visible (non-toast) window");
-            }
-        }
-
-        String realCallingPackage = mService.getPackageNameIfUnique(realCallingUid, realCallingPid);
-
-        // Legacy behavior allows to use caller foreground state to bypass BAL restriction.
-        // The options here are the options passed by the sender and not those on the intent.
-        final BackgroundStartPrivileges balAllowedByPiSender =
-                PendingIntentRecord.getBackgroundStartPrivilegesAllowedByCaller(
-                        checkedOptions, realCallingUid, realCallingPackage);
-
-        final boolean logVerdictChangeByPiDefaultChange = checkedOptions == null
-                || checkedOptions.getPendingIntentBackgroundActivityStartMode()
-                        == ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
-        final boolean considerPiRules = logVerdictChangeByPiDefaultChange
-                || balAllowedByPiSender.allowsBackgroundActivityStarts();
-        final String verdictLogForPiSender =
-                balAllowedByPiSender.allowsBackgroundActivityStarts() ? VERDICT_ALLOWED
-                        : VERDICT_WOULD_BE_ALLOWED_IF_SENDER_GRANTS_BAL;
-
-        @BalCode int resultIfPiSenderAllowsBal = BAL_BLOCK;
-        if (realCallingUid != callingUid && considerPiRules) {
-            resultIfPiSenderAllowsBal = checkPiBackgroundActivityStart(callingUid, realCallingUid,
-                backgroundStartPrivileges, intent, checkedOptions,
-                realCallingUidHasAnyVisibleWindow, isRealCallingUidPersistentSystemProcess,
-                verdictLogForPiSender);
-        }
-        if (resultIfPiSenderAllowsBal != BAL_BLOCK
-                && balAllowedByPiSender.allowsBackgroundActivityStarts()
-                && !logVerdictChangeByPiDefaultChange) {
-            // The result is to allow (because the sender allows BAL) and we are not interested in
-            // logging differences, so just return.
-            return resultIfPiSenderAllowsBal;
-        }
-        if (useCallingUidState) {
-            // don't abort if the callingUid has START_ACTIVITIES_FROM_BACKGROUND permission
-            if (ActivityTaskManagerService.checkPermission(START_ACTIVITIES_FROM_BACKGROUND,
-                    callingPid, callingUid) == PERMISSION_GRANTED) {
-                return logStartAllowedAndReturnCode(BAL_ALLOW_PERMISSION,
-                    resultIfPiSenderAllowsBal, balAllowedByPiSender,
-                    /*background*/ true, callingUid, realCallingUid, intent,
+        // don't abort if the callingUid has START_ACTIVITIES_FROM_BACKGROUND permission
+        if (ActivityTaskManagerService.checkPermission(START_ACTIVITIES_FROM_BACKGROUND,
+                callingPid, callingUid) == PERMISSION_GRANTED) {
+            return new BalVerdict(BAL_ALLOW_PERMISSION,
+                    /*background*/ true,
                     "START_ACTIVITIES_FROM_BACKGROUND permission granted");
-            }
-            // don't abort if the caller has the same uid as the recents component
-            if (mSupervisor.mRecentTasks.isCallerRecents(callingUid)) {
-                return logStartAllowedAndReturnCode(
-                    BAL_ALLOW_ALLOWLISTED_COMPONENT,
-                    resultIfPiSenderAllowsBal, balAllowedByPiSender,
-                    /*background*/ true, callingUid, realCallingUid,
-                    intent, "Recents Component");
-            }
-            // don't abort if the callingUid is the device owner
-            if (mService.isDeviceOwner(callingUid)) {
-                return logStartAllowedAndReturnCode(
-                    BAL_ALLOW_ALLOWLISTED_COMPONENT,
-                    resultIfPiSenderAllowsBal, balAllowedByPiSender,
-                    /*background*/ true, callingUid, realCallingUid,
-                    intent, "Device Owner");
-            }
-            // don't abort if the callingUid is a affiliated profile owner
-            if (mService.isAffiliatedProfileOwner(callingUid)) {
-                return logStartAllowedAndReturnCode(
-                    BAL_ALLOW_ALLOWLISTED_COMPONENT,
-                    resultIfPiSenderAllowsBal, balAllowedByPiSender,
-                    /*background*/ true, callingUid, realCallingUid,
-                    intent, "Affiliated Profile Owner");
-            }
-            // don't abort if the callingUid has companion device
-            final int callingUserId = UserHandle.getUserId(callingUid);
-            if (mService.isAssociatedCompanionApp(callingUserId, callingUid)) {
-                return logStartAllowedAndReturnCode(
-                    BAL_ALLOW_ALLOWLISTED_COMPONENT,
-                    resultIfPiSenderAllowsBal, balAllowedByPiSender,
-                    /*background*/ true, callingUid, realCallingUid,
-                    intent, "Companion App");
-            }
-            // don't abort if the callingUid has SYSTEM_ALERT_WINDOW permission
-            if (mService.hasSystemAlertWindowPermission(callingUid, callingPid, callingPackage)) {
-                Slog.w(
-                        TAG,
-                        "Background activity start for "
-                                + callingPackage
-                                + " allowed because SYSTEM_ALERT_WINDOW permission is granted.");
-                return logStartAllowedAndReturnCode(
-                    BAL_ALLOW_SAW_PERMISSION,
-                    resultIfPiSenderAllowsBal, balAllowedByPiSender,
-                    /*background*/ true, callingUid, realCallingUid,
-                    intent, "SYSTEM_ALERT_WINDOW permission is granted");
-            }
-            // don't abort if the callingUid and callingPackage have the
-            // OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION appop
-            if (isSystemExemptFlagEnabled() && mService.getAppOpsManager().checkOpNoThrow(
-                        AppOpsManager.OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION,
-                        callingUid, callingPackage) == AppOpsManager.MODE_ALLOWED) {
-                return logStartAllowedAndReturnCode(BAL_ALLOW_PERMISSION,
-                        resultIfPiSenderAllowsBal, balAllowedByPiSender,
-                        /*background*/ true, callingUid, realCallingUid, intent,
-                        "OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION appop is granted");
-            }
         }
+        // don't abort if the caller has the same uid as the recents component
+        if (mSupervisor.mRecentTasks.isCallerRecents(callingUid)) {
+            return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
+                    /*background*/ true, "Recents Component");
+        }
+        // don't abort if the callingUid is the device owner
+        if (mService.isDeviceOwner(callingUid)) {
+            return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
+                    /*background*/ true, "Device Owner");
+        }
+        // don't abort if the callingUid is a affiliated profile owner
+        if (mService.isAffiliatedProfileOwner(callingUid)) {
+            return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
+                    /*background*/ true, "Affiliated Profile Owner");
+        }
+        // don't abort if the callingUid has companion device
+        final int callingUserId = UserHandle.getUserId(callingUid);
+        if (mService.isAssociatedCompanionApp(callingUserId, callingUid)) {
+            return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
+                    /*background*/ true, "Companion App");
+        }
+        // don't abort if the callingUid has SYSTEM_ALERT_WINDOW permission
+        if (mService.hasSystemAlertWindowPermission(callingUid, callingPid, callingPackage)) {
+            Slog.w(
+                    TAG,
+                    "Background activity start for "
+                            + callingPackage
+                            + " allowed because SYSTEM_ALERT_WINDOW permission is granted.");
+            return new BalVerdict(BAL_ALLOW_SAW_PERMISSION,
+                    /*background*/ true, "SYSTEM_ALERT_WINDOW permission is granted");
+        }
+        // don't abort if the callingUid and callingPackage have the
+        // OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION appop
+        if (isSystemExemptFlagEnabled() && mService.getAppOpsManager().checkOpNoThrow(
+                AppOpsManager.OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION,
+                callingUid, callingPackage) == AppOpsManager.MODE_ALLOWED) {
+            return new BalVerdict(BAL_ALLOW_PERMISSION, /*background*/ true,
+                    "OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION appop is granted");
+        }
+
         // If we don't have callerApp at this point, no caller was provided to startActivity().
         // That's the case for PendingIntent-based starts, since the creator's process might not be
-        // up and alive. If that's the case, we retrieve the WindowProcessController for the send()
-        // caller if caller allows, so that we can make the decision based on its state.
-        int callerAppUid = callingUid;
-        boolean callerAppBasedOnPiSender = callerApp == null && considerPiRules
-                && resultIfPiSenderAllowsBal == BAL_BLOCK;
-        if (callerAppBasedOnPiSender) {
-            callerApp = mService.getProcessController(realCallingPid, realCallingUid);
-            callerAppUid = realCallingUid;
-        }
-        // don't abort if the callerApp or other processes of that uid are allowed in any way
-        if (callerApp != null && (useCallingUidState || callerAppBasedOnPiSender)) {
-            // first check the original calling process
-            final @BalCode int balAllowedForCaller = callerApp
-                    .areBackgroundActivityStartsAllowed(appSwitchState);
-            if (balAllowedForCaller != BAL_BLOCK) {
-                if (callerAppBasedOnPiSender) {
-                    resultIfPiSenderAllowsBal = logStartAllowedAndReturnCode(balAllowedForCaller,
-                        /*background*/ true, callingUid, realCallingUid, intent,
-                        "callerApp process (pid = " + callerApp.getPid()
-                            + ", uid = " + callerAppUid + ") is allowed", verdictLogForPiSender);
-                } else {
-                    return logStartAllowedAndReturnCode(balAllowedForCaller,
-                        resultIfPiSenderAllowsBal, balAllowedByPiSender,
-                        /*background*/ true, callingUid, realCallingUid, intent,
-                        "callerApp process (pid = " + callerApp.getPid()
-                            + ", uid = " + callerAppUid + ") is allowed");
-                }
-            } else {
-                // only if that one wasn't allowed, check the other ones
-                final ArraySet<WindowProcessController> uidProcesses =
-                    mService.mProcessMap.getProcesses(callerAppUid);
-                if (uidProcesses != null) {
-                    for (int i = uidProcesses.size() - 1; i >= 0; i--) {
-                        final WindowProcessController proc = uidProcesses.valueAt(i);
-                        int balAllowedForUid = proc.areBackgroundActivityStartsAllowed(
-                                appSwitchState);
-                        if (proc != callerApp && balAllowedForUid != BAL_BLOCK) {
-                            if (callerAppBasedOnPiSender) {
-                                resultIfPiSenderAllowsBal = logStartAllowedAndReturnCode(
-                                    balAllowedForUid,
-                                    /*background*/ true, callingUid, realCallingUid, intent,
-                                    "process" + proc.getPid() + " from uid " + callerAppUid
-                                        + " is allowed", verdictLogForPiSender);
-                                break;
-                            } else {
-                                return logStartAllowedAndReturnCode(balAllowedForUid,
-                                    resultIfPiSenderAllowsBal, balAllowedByPiSender,
-                                    /*background*/ true, callingUid, realCallingUid, intent,
-                                    "process" + proc.getPid() + " from uid " + callerAppUid
-                                        + " is allowed");
-                            }
-                        }
-                    }
-                }
-            }
-            if (callerAppBasedOnPiSender) {
-                // If caller app was based on PI sender, this result is part of
-                // resultIfPiSenderAllowsBal
-                if (resultIfPiSenderAllowsBal != BAL_BLOCK
-                        && balAllowedByPiSender.allowsBackgroundActivityStarts()
-                        && !logVerdictChangeByPiDefaultChange) {
-                    // The result is to allow (because the sender allows BAL) and we are not
-                    // interested in logging differences, so just return.
-                    return resultIfPiSenderAllowsBal;
-                }
-            } else {
-                // If caller app was NOT based on PI sender and we found a allow reason we should
-                // have returned already
-                checkState(balAllowedForCaller == BAL_BLOCK,
-                        "balAllowedForCaller = " + balAllowedForCaller + " (should have returned)");
-            }
-        }
-        // If we are here, it means all exemptions not based on PI sender failed, so we'll block
-        // unless resultIfPiSenderAllowsBal is an allow and the PI sender allows BAL
-
-        if (realCallingPackage == null) {
-            realCallingPackage = (callingUid == realCallingUid ? callingPackage :
-                    mService.mContext.getPackageManager().getNameForUid(realCallingUid))
-                    + "[debugOnly]";
+        // up and alive.
+        // Don't abort if the callerApp or other processes of that uid are allowed in any way.
+        BalVerdict callerAppAllowsBal = checkProcessAllowsBal(callerApp, state);
+        if (callerAppAllowsBal.allows()) {
+            return callerAppAllowsBal;
         }
 
-        String stateDumpLog = " [callingPackage: " + callingPackage
-                + "; callingUid: " + callingUid
-                + "; appSwitchState: " + appSwitchState
-                + "; callingUidHasAnyVisibleWindow: " + callingUidHasAnyVisibleWindow
-                + "; callingUidProcState: " + DebugUtils.valueToString(
-                        ActivityManager.class, "PROCESS_STATE_", callingUidProcState)
-                + "; isCallingUidPersistentSystemProcess: " + isCallingUidPersistentSystemProcess
-                + "; balAllowedByPiSender: " + balAllowedByPiSender
-                + "; realCallingPackage: " + realCallingPackage
-                + "; realCallingUid: " + realCallingUid
-                + "; realCallingUidHasAnyVisibleWindow: " + realCallingUidHasAnyVisibleWindow
-                + "; realCallingUidProcState: " + DebugUtils.valueToString(
-                        ActivityManager.class, "PROCESS_STATE_", realCallingUidProcState)
-                + "; isRealCallingUidPersistentSystemProcess: "
-                        + isRealCallingUidPersistentSystemProcess
-                + "; originatingPendingIntent: " + originatingPendingIntent
-                + "; backgroundStartPrivileges: " + backgroundStartPrivileges
-                + "; intent: " + intent
-                + "; callerApp: " + callerApp
-                + "; inVisibleTask: " + (callerApp != null && callerApp.hasActivityInVisibleTask())
-                + "; resultIfPiSenderAllowsBal: " + balCodeToString(resultIfPiSenderAllowsBal)
-                + "]";
-        if (resultIfPiSenderAllowsBal != BAL_BLOCK) {
-            // We should have returned before if !logVerdictChangeByPiDefaultChange
-            checkState(logVerdictChangeByPiDefaultChange,
-                    "resultIfPiSenderAllowsBal = " + balCodeToString(resultIfPiSenderAllowsBal)
-                        + " at the end but logVerdictChangeByPiDefaultChange = false");
-            if (balAllowedByPiSender.allowsBackgroundActivityStarts()) {
-                // The verdict changed from block to allow, PI sender default change is off and
-                // we'd block if it were on
-                Slog.wtf(TAG, "With BAL hardening this activity start would be blocked!"
-                        + stateDumpLog);
-                return resultIfPiSenderAllowsBal;
-            } else {
-                // The verdict changed from allow (resultIfPiSenderAllowsBal) to block, PI sender
-                // default change is on (otherwise we would have fallen into if above) and we'd
-                // allow if it were off
-                Slog.wtf(TAG, "Without BAL hardening this activity start would be allowed!"
-                        + stateDumpLog);
-            }
-        }
-        // anything that has fallen through would currently be aborted
-        Slog.w(TAG, "Background activity launch blocked" + stateDumpLog);
-        // log aborted activity start to TRON
-        if (mService.isActivityStartsLoggingEnabled()) {
-            mSupervisor
-                    .getActivityMetricsLogger()
-                    .logAbortedBgActivityStart(
-                            intent,
-                            callerApp,
-                            callingUid,
-                            callingPackage,
-                            callingUidProcState,
-                            callingUidHasAnyVisibleWindow,
-                            realCallingUid,
-                            realCallingUidProcState,
-                            realCallingUidHasAnyVisibleWindow,
-                            (originatingPendingIntent != null));
-        }
-        return BAL_BLOCK;
+        // If we are here, it means all exemptions based on the creator failed
+        return BalVerdict.BLOCK;
     }
 
-    private @BalCode int checkPiBackgroundActivityStart(int callingUid, int realCallingUid,
-            BackgroundStartPrivileges backgroundStartPrivileges, Intent intent,
-            ActivityOptions checkedOptions, boolean realCallingUidHasAnyVisibleWindow,
-            boolean isRealCallingUidPersistentSystemProcess, String verdictLog) {
-        final boolean useCallerPermission =
-                PendingIntentRecord.isPendingIntentBalAllowedByPermission(checkedOptions);
-        if (useCallerPermission
+    /**
+     * @return A code denoting which BAL rule allows an activity to be started,
+     * or {@link #BAL_BLOCK} if the launch should be blocked
+     */
+    BalVerdict checkBackgroundActivityStartAllowedBySender(
+            BalState state,
+            ActivityOptions checkedOptions) {
+        int realCallingUid = state.mRealCallingUid;
+        BackgroundStartPrivileges backgroundStartPrivileges = state.mBackgroundStartPrivileges;
+
+        if (PendingIntentRecord.isPendingIntentBalAllowedByPermission(checkedOptions)
                 && ActivityManager.checkComponentPermission(
-                                android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND,
+                android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND,
                 realCallingUid, -1, true) == PackageManager.PERMISSION_GRANTED) {
-            return logStartAllowedAndReturnCode(BAL_ALLOW_PENDING_INTENT,
-                /*background*/ false, callingUid, realCallingUid, intent,
-                "realCallingUid has BAL permission. realCallingUid: " + realCallingUid,
-                verdictLog);
+            return new BalVerdict(BAL_ALLOW_PENDING_INTENT,
+                    /*background*/ false,
+                    "realCallingUid has BAL permission. realCallingUid: " + realCallingUid);
         }
 
         // don't abort if the realCallingUid has a visible window
         // TODO(b/171459802): We should check appSwitchAllowed also
-        if (realCallingUidHasAnyVisibleWindow) {
-            return logStartAllowedAndReturnCode(BAL_ALLOW_PENDING_INTENT,
-                    /*background*/ false, callingUid, realCallingUid, intent,
+        if (state.mRealCallingUidHasAnyVisibleWindow) {
+            return new BalVerdict(BAL_ALLOW_PENDING_INTENT,
+                    /*background*/ false,
                     "realCallingUid has visible (non-toast) window. realCallingUid: "
-                            + realCallingUid, verdictLog);
+                            + realCallingUid);
         }
         // if the realCallingUid is a persistent system process, abort if the IntentSender
         // wasn't allowed to start an activity
-        if (isRealCallingUidPersistentSystemProcess
+        if (state.mIsRealCallingUidPersistentSystemProcess
                 && backgroundStartPrivileges.allowsBackgroundActivityStarts()) {
-            return logStartAllowedAndReturnCode(BAL_ALLOW_PENDING_INTENT,
-                    /*background*/ false, callingUid, realCallingUid, intent,
+            return new BalVerdict(BAL_ALLOW_PENDING_INTENT,
+                    /*background*/ false,
                     "realCallingUid is persistent system process AND intent "
                             + "sender allowed (allowBackgroundActivityStart = true). "
-                            + "realCallingUid: " + realCallingUid, verdictLog);
+                            + "realCallingUid: " + realCallingUid);
         }
         // don't abort if the realCallingUid is an associated companion app
         if (mService.isAssociatedCompanionApp(
                 UserHandle.getUserId(realCallingUid), realCallingUid)) {
-            return logStartAllowedAndReturnCode(BAL_ALLOW_PENDING_INTENT,
-                    /*background*/ false, callingUid, realCallingUid, intent,
+            return new BalVerdict(BAL_ALLOW_PENDING_INTENT,
+                    /*background*/ false,
                     "realCallingUid is a companion app. "
-                            + "realCallingUid: " + realCallingUid, verdictLog);
+                            + "realCallingUid: " + realCallingUid);
         }
-        return BAL_BLOCK;
+
+        // don't abort if the callerApp or other processes of that uid are allowed in any way
+        BalVerdict realCallerAppAllowsBal =
+                checkProcessAllowsBal(state.mRealCallerApp, state);
+        if (realCallerAppAllowsBal.allows()) {
+            return realCallerAppAllowsBal;
+        }
+
+        // If we are here, it means all exemptions based on PI sender failed
+        return BalVerdict.BLOCK;
+    }
+
+    /**
+     * Check if the app allows BAL.
+     *
+     * See {@link BackgroundLaunchProcessController#areBackgroundActivityStartsAllowed(int, int,
+     * String, int, boolean, boolean, boolean, long, long, long)} for details on the
+     * exceptions.
+     */
+    private BalVerdict checkProcessAllowsBal(WindowProcessController app,
+            BalState state) {
+        if (app == null) {
+            return BalVerdict.BLOCK;
+        }
+        // first check the original calling process
+        final BalVerdict balAllowedForCaller = app
+                .areBackgroundActivityStartsAllowed(state.mAppSwitchState);
+        if (balAllowedForCaller.allows()) {
+            return balAllowedForCaller.withProcessInfo("callerApp process", app);
+        } else {
+            // only if that one wasn't allowed, check the other ones
+            final ArraySet<WindowProcessController> uidProcesses =
+                    mService.mProcessMap.getProcesses(app.mUid);
+            if (uidProcesses != null) {
+                for (int i = uidProcesses.size() - 1; i >= 0; i--) {
+                    final WindowProcessController proc = uidProcesses.valueAt(i);
+                    if (proc != app) {
+                        BalVerdict balAllowedForUid = proc.areBackgroundActivityStartsAllowed(
+                                state.mAppSwitchState);
+                        if (balAllowedForUid.allows()) {
+                            return balAllowedForCaller.withProcessInfo("process", proc);
+                        }
+                    }
+                }
+            }
+        }
+        return BalVerdict.BLOCK;
     }
 
     /**
@@ -1091,55 +1209,6 @@
         return joiner.toString();
     }
 
-    static @BalCode int logStartAllowedAndReturnCode(@BalCode int code, boolean background,
-            int callingUid, int realCallingUid, Intent intent, int pid, String msg) {
-        return logStartAllowedAndReturnCode(code, background, callingUid, realCallingUid, intent,
-                DEBUG_ACTIVITY_STARTS ?  ("[Process(" + pid + ")]" + msg) : "");
-    }
-
-    static @BalCode int logStartAllowedAndReturnCode(@BalCode int code, boolean background,
-            int callingUid, int realCallingUid, Intent intent, String msg) {
-        return logStartAllowedAndReturnCode(code, background, callingUid, realCallingUid, intent,
-            msg, VERDICT_ALLOWED);
-    }
-
-    /**
-     * Logs the start and returns one of the provided codes depending on if the PI sender allows
-     * using its BAL privileges.
-     */
-    static @BalCode int logStartAllowedAndReturnCode(@BalCode int result,
-            @BalCode int resultIfPiSenderAllowsBal, BackgroundStartPrivileges balAllowedByPiSender,
-            boolean background, int callingUid, int realCallingUid, Intent intent, String msg) {
-        if (resultIfPiSenderAllowsBal != BAL_BLOCK
-                && balAllowedByPiSender.allowsBackgroundActivityStarts()) {
-            // resultIfPiSenderAllowsBal was already logged, so just return
-            return resultIfPiSenderAllowsBal;
-        }
-        return logStartAllowedAndReturnCode(result, background, callingUid, realCallingUid,
-            intent, msg, VERDICT_ALLOWED);
-    }
-
-
-    static @BalCode int logStartAllowedAndReturnCode(@BalCode int code, boolean background,
-            int callingUid, int realCallingUid, Intent intent, String msg, String verdict) {
-        statsLogBalAllowed(code, callingUid, realCallingUid, intent);
-        if (DEBUG_ACTIVITY_STARTS) {
-            StringBuilder builder = new StringBuilder();
-            if (background) {
-                builder.append("Background ");
-            }
-            builder.append(verdict + ": " + msg + ". callingUid: " + callingUid + ". ");
-            builder.append("BAL Code: ");
-            builder.append(balCodeToString(code));
-            if (verdict.equals(VERDICT_ALLOWED)) {
-                Slog.i(TAG, builder.toString());
-            } else {
-                Slog.d(TAG, builder.toString());
-            }
-        }
-        return code;
-    }
-
     private static boolean isSystemExemptFlagEnabled() {
         return DeviceConfig.getBoolean(
                 NAMESPACE_WINDOW_MANAGER,
@@ -1147,8 +1216,12 @@
                 /* defaultValue= */ true);
     }
 
-    private static void statsLogBalAllowed(
-            @BalCode int code, int callingUid, int realCallingUid, Intent intent) {
+    private static BalVerdict statsLog(BalVerdict finalVerdict, BalState state) {
+        @BalCode int code = finalVerdict.getCode();
+        int callingUid = state.mCallingUid;
+        int realCallingUid = state.mRealCallingUid;
+        Intent intent = state.mIntent;
+
         if (code == BAL_ALLOW_PENDING_INTENT
                 && (callingUid == Process.SYSTEM_UID || realCallingUid == Process.SYSTEM_UID)) {
             String activityName =
@@ -1168,6 +1241,7 @@
                     callingUid,
                     realCallingUid);
         }
+        return finalVerdict;
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java b/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java
index 527edc1..e849589 100644
--- a/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java
+++ b/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java
@@ -27,7 +27,6 @@
 import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_GRACE_PERIOD;
 import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_PERMISSION;
 import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_VISIBLE_WINDOW;
-import static com.android.server.wm.BackgroundActivityStartController.BAL_BLOCK;
 
 import static java.util.Objects.requireNonNull;
 
@@ -49,6 +48,7 @@
 import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.server.wm.BackgroundActivityStartController.BalVerdict;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -96,37 +96,33 @@
         mBackgroundActivityStartCallback = callback;
     }
 
-    @BackgroundActivityStartController.BalCode
-    int areBackgroundActivityStartsAllowed(int pid, int uid, String packageName,
+    BalVerdict areBackgroundActivityStartsAllowed(
+            int pid, int uid, String packageName,
             int appSwitchState, boolean isCheckingForFgsStart,
             boolean hasActivityInVisibleTask, boolean hasBackgroundActivityStartPrivileges,
             long lastStopAppSwitchesTime, long lastActivityLaunchTime,
             long lastActivityFinishTime) {
         // Allow if the proc is instrumenting with background activity starts privs.
         if (hasBackgroundActivityStartPrivileges) {
-            return BackgroundActivityStartController.logStartAllowedAndReturnCode(
-                    BAL_ALLOW_PERMISSION, /*background*/ true, uid, uid, /*intent*/ null,
-                    pid, "Activity start allowed: process instrumenting with background "
-                        + "activity starts privileges");
+            return new BalVerdict(BAL_ALLOW_PERMISSION, /*background*/ true,
+                    "Activity start allowed: process instrumenting with background "
+                            + "activity starts privileges");
         }
         // Allow if the flag was explicitly set.
         if (isBackgroundStartAllowedByToken(uid, packageName, isCheckingForFgsStart)) {
-            return BackgroundActivityStartController.logStartAllowedAndReturnCode(
-                    BAL_ALLOW_PERMISSION, /*background*/ true, uid, uid, /*intent*/ null,
-                    pid, "Activity start allowed: process allowed by token");
+            return new BalVerdict(BAL_ALLOW_PERMISSION, /*background*/ true,
+                    "Activity start allowed: process allowed by token");
         }
         // Allow if the caller is bound by a UID that's currently foreground.
         if (isBoundByForegroundUid()) {
-            return BackgroundActivityStartController.logStartAllowedAndReturnCode(
-                    BAL_ALLOW_VISIBLE_WINDOW, /*background*/ false, uid, uid, /*intent*/ null,
-                    pid, "Activity start allowed: process bound by foreground uid");
+            return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW, /*background*/ false,
+                    "Activity start allowed: process bound by foreground uid");
         }
         // Allow if the caller has an activity in any foreground task.
         if (hasActivityInVisibleTask
                 && (appSwitchState == APP_SWITCH_ALLOW || appSwitchState == APP_SWITCH_FG_ONLY)) {
-            return BackgroundActivityStartController.logStartAllowedAndReturnCode(
-                    BAL_ALLOW_FOREGROUND, /*background*/ false, uid, uid, /*intent*/ null,
-                    pid, "Activity start allowed: process has activity in foreground task");
+            return new BalVerdict(BAL_ALLOW_FOREGROUND, /*background*/ false,
+                    "Activity start allowed: process has activity in foreground task");
         }
 
         // If app switching is not allowed, we ignore all the start activity grace period
@@ -141,9 +137,8 @@
                 // let app to be able to start background activity even it's in grace period.
                 if (lastActivityLaunchTime > lastStopAppSwitchesTime
                         || lastActivityFinishTime > lastStopAppSwitchesTime) {
-                    return BackgroundActivityStartController.logStartAllowedAndReturnCode(
-                            BAL_ALLOW_GRACE_PERIOD, /*background*/ true, uid, uid, /*intent*/ null,
-                            pid, "Activity start allowed: within "
+                    return new BalVerdict(BAL_ALLOW_GRACE_PERIOD, /*background*/ true,
+                            "Activity start allowed: within "
                                     + ACTIVITY_BG_START_GRACE_PERIOD_MS + "ms grace period");
                 }
                 if (DEBUG_ACTIVITY_STARTS) {
@@ -154,7 +149,7 @@
 
             }
         }
-        return BAL_BLOCK;
+        return BalVerdict.BLOCK;
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 4fa6e29..f314900 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -1622,7 +1622,17 @@
             return;
         }
 
+        final Transition.ReadyCondition displayConfig = mTransitionController.isCollecting()
+                ? new Transition.ReadyCondition("displayConfig", this) : null;
+        if (displayConfig != null) {
+            mTransitionController.waitFor(displayConfig);
+        } else if (mTransitionController.isShellTransitionsEnabled()) {
+            Slog.e(TAG, "Display reconfigured outside of a transition: " + this);
+        }
         final boolean configUpdated = updateDisplayOverrideConfigurationLocked();
+        if (displayConfig != null) {
+            displayConfig.meet();
+        }
         if (configUpdated) {
             return;
         }
@@ -4706,6 +4716,17 @@
             scheduleAnimation();
 
             mWmService.mH.post(() -> InputMethodManagerInternal.get().onImeParentChanged());
+        } else if (mImeControlTarget != null && mImeControlTarget == mImeLayeringTarget) {
+            // Even if the IME surface parent is not changed, the layer target belonging to the
+            // parent may have changes. Then attempt to reassign if the IME control target is
+            // possible to be the relative layer.
+            final SurfaceControl lastRelativeLayer = mImeWindowsContainer.getLastRelativeLayer();
+            if (lastRelativeLayer != mImeLayeringTarget.mSurfaceControl) {
+                assignRelativeLayerForIme(getSyncTransaction(), false /* forceUpdate */);
+                if (lastRelativeLayer != mImeWindowsContainer.getLastRelativeLayer()) {
+                    scheduleAnimation();
+                }
+            }
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 17bfeb4..d69c5ef 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -1034,20 +1034,15 @@
                 }
                 break;
             case TYPE_NAVIGATION_BAR:
-                mContext.enforcePermission(
-                        android.Manifest.permission.STATUS_BAR_SERVICE, callingPid, callingUid,
-                        "DisplayPolicy");
+                mContext.enforcePermission(android.Manifest.permission.STATUS_BAR_SERVICE,
+                        callingPid, callingUid, "DisplayPolicy");
                 if (mNavigationBar != null && mNavigationBar.isAlive()) {
                     return WindowManagerGlobal.ADD_MULTIPLE_SINGLETON;
                 }
                 break;
             case TYPE_NAVIGATION_BAR_PANEL:
-                // Check for permission if the caller is not the recents component.
-                if (!mService.mAtmService.isCallerRecents(callingUid)) {
-                    mContext.enforcePermission(
-                            android.Manifest.permission.STATUS_BAR_SERVICE, callingPid, callingUid,
-                            "DisplayPolicy");
-                }
+                mContext.enforcePermission(android.Manifest.permission.STATUS_BAR_SERVICE,
+                        callingPid, callingUid, "DisplayPolicy");
                 break;
             case TYPE_STATUS_BAR_ADDITIONAL:
             case TYPE_STATUS_BAR_SUB_PANEL:
diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java
index 1a319ad..fa2c94a 100644
--- a/services/core/java/com/android/server/wm/KeyguardController.java
+++ b/services/core/java/com/android/server/wm/KeyguardController.java
@@ -87,6 +87,7 @@
     private RootWindowContainer mRootWindowContainer;
     private final ActivityTaskManagerInternal.SleepTokenAcquirer mSleepTokenAcquirer;
     private boolean mWaitingForWakeTransition;
+    private Transition.ReadyCondition mWaitAodHide = null;
 
     KeyguardController(ActivityTaskManagerService service,
             ActivityTaskSupervisor taskSupervisor) {
@@ -565,8 +566,14 @@
 
     // Defer transition until AOD dismissed.
     void updateDeferTransitionForAod(boolean waiting) {
-        if (waiting == mWaitingForWakeTransition) {
-            return;
+        if (mService.getTransitionController().useFullReadyTracking()) {
+            if (waiting == (mWaitAodHide != null)) {
+                return;
+            }
+        } else {
+            if (waiting == mWaitingForWakeTransition) {
+                return;
+            }
         }
         if (!mService.getTransitionController().isCollecting()) {
             return;
@@ -575,12 +582,17 @@
         if (waiting && isAodShowing(DEFAULT_DISPLAY)) {
             mWaitingForWakeTransition = true;
             mWindowManager.mAtmService.getTransitionController().deferTransitionReady();
+            mWaitAodHide = new Transition.ReadyCondition("AOD hidden");
+            mWindowManager.mAtmService.getTransitionController().waitFor(mWaitAodHide);
             mWindowManager.mH.postDelayed(mResetWaitTransition, DEFER_WAKE_TRANSITION_TIMEOUT_MS);
         } else if (!waiting) {
             // dismiss the deferring if the AOD state change or cancel awake.
             mWaitingForWakeTransition = false;
             mWindowManager.mAtmService.getTransitionController().continueTransitionReady();
             mWindowManager.mH.removeCallbacks(mResetWaitTransition);
+            final Transition.ReadyCondition waitAodHide = mWaitAodHide;
+            mWaitAodHide = null;
+            waitAodHide.meet();
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 2281395..c81105a 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -2051,6 +2051,8 @@
         }
 
         transitionController.deferTransitionReady();
+        Transition.ReadyCondition pipChangesApplied = new Transition.ReadyCondition("movedToPip");
+        transitionController.waitFor(pipChangesApplied);
         mService.deferWindowLayout();
         try {
             // This will change the root pinned task's windowing mode to its original mode, ensuring
@@ -2235,6 +2237,7 @@
                 }
             } finally {
                 transitionController.continueTransitionReady();
+                pipChangesApplied.meet();
             }
         }
 
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 6c31e3e..fa104bb 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -3643,7 +3643,7 @@
      */
     TaskFragmentParentInfo getTaskFragmentParentInfo() {
         return new TaskFragmentParentInfo(getConfiguration(), getDisplayId(),
-                shouldBeVisible(null /* starting */));
+                shouldBeVisible(null /* starting */), hasNonFinishingDirectActivity());
     }
 
     @Override
@@ -4715,7 +4715,7 @@
         }
         if (isAttached()) {
             setWindowingMode(WINDOWING_MODE_UNDEFINED);
-            moveTaskToBackInner(this);
+            moveTaskToBackInner(this, null /* transition */);
         }
         if (top.isAttached()) {
             top.setWindowingMode(WINDOWING_MODE_UNDEFINED);
@@ -5718,24 +5718,27 @@
                         mTransitionController.requestStartTransition(transition, tr,
                                 null /* remoteTransition */, null /* displayChange */);
                         mTransitionController.collect(tr);
-                        moveTaskToBackInner(tr);
+                        moveTaskToBackInner(tr, transition);
                     });
         } else {
             // Skip the transition for pinned task.
             if (!inPinnedWindowingMode()) {
                 mDisplayContent.prepareAppTransition(TRANSIT_TO_BACK);
             }
-            moveTaskToBackInner(tr);
+            moveTaskToBackInner(tr, null /* transition */);
         }
         return true;
     }
 
-    private boolean moveTaskToBackInner(@NonNull Task task) {
-        if (mTransitionController.isShellTransitionsEnabled()) {
+    private boolean moveTaskToBackInner(@NonNull Task task, @Nullable Transition transition) {
+        final Transition.ReadyCondition movedToBack =
+                new Transition.ReadyCondition("moved-to-back", task);
+        if (transition != null) {
             // Preventing from update surface position for WindowState if configuration changed,
             // because the position is depends on WindowFrame, so update the position before
             // relayout will only update it to "old" position.
             mAtmService.deferWindowLayout();
+            transition.mReadyTracker.add(movedToBack);
         }
         try {
             moveToBack("moveTaskToBackInner", task);
@@ -5752,6 +5755,9 @@
             if (mTransitionController.isShellTransitionsEnabled()) {
                 mAtmService.continueWindowLayout();
             }
+            if (transition != null) {
+                movedToBack.meet();
+            }
         }
         ActivityRecord topActivity = getDisplayArea().topRunningActivity();
         Task topRootTask = topActivity == null ? null : topActivity.getRootTask();
@@ -6056,6 +6062,11 @@
             // Non-root task position changed.
             mRootWindowContainer.invalidateTaskLayers();
         }
+
+        if (child.asActivityRecord() != null) {
+            // Send for TaskFragmentParentInfo#hasDirectActivity change.
+            sendTaskFragmentParentInfoChangedIfNeeded();
+        }
     }
 
     void reparent(TaskDisplayArea newParent, boolean onTop) {
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 82d3424..2fc531a 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -1049,6 +1049,10 @@
         return getActivity(ActivityRecord::canBeTopRunning);
     }
 
+    /**
+     * Reports non-finishing activity count including this TaskFragment's child embedded
+     * TaskFragments' children activities.
+     */
     int getNonFinishingActivityCount() {
         final int[] runningActivityCount = new int[1];
         forAllActivities(a -> {
@@ -1059,6 +1063,20 @@
         return runningActivityCount[0];
     }
 
+    /**
+     * Returns {@code true} if there's any non-finishing direct children activity, which is not
+     * embedded in TaskFragments
+     */
+    boolean hasNonFinishingDirectActivity() {
+        for (int i = getChildCount() - 1; i >= 0; --i) {
+            final ActivityRecord activity = getChildAt(i).asActivityRecord();
+            if (activity != null && !activity.finishing) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     boolean isTopActivityFocusable() {
         final ActivityRecord r = topRunningActivity();
         return r != null ? r.isFocusable()
diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
index ff766be..34ae370 100644
--- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
@@ -157,6 +157,15 @@
          */
         private final ArrayMap<IBinder, Integer> mDeferredTransitions = new ArrayMap<>();
 
+        /**
+         * Map from {@link TaskFragmentTransaction#getTransactionToken()} to a
+         * {@link Transition.ReadyCondition} that is waiting for the {@link TaskFragmentTransaction}
+         * to complete.
+         * @see #onTransactionHandled
+         */
+        private final ArrayMap<IBinder, Transition.ReadyCondition> mInFlightTransactions =
+                new ArrayMap<>();
+
         TaskFragmentOrganizerState(@NonNull ITaskFragmentOrganizer organizer, int pid, int uid,
                 boolean isSystemOrganizer) {
             mOrganizer = organizer;
@@ -173,7 +182,7 @@
         @Override
         public void binderDied() {
             synchronized (mGlobalLock) {
-                removeOrganizer(mOrganizer);
+                removeOrganizer(mOrganizer, "client died");
             }
         }
 
@@ -195,7 +204,7 @@
             mOrganizedTaskFragments.remove(taskFragment);
         }
 
-        void dispose() {
+        void dispose(@NonNull String reason) {
             boolean wasVisible = false;
             for (int i = mOrganizedTaskFragments.size() - 1; i >= 0; i--) {
                 final TaskFragment taskFragment = mOrganizedTaskFragments.get(i);
@@ -236,6 +245,10 @@
                 // Cleanup any running transaction to unblock the current transition.
                 onTransactionFinished(mDeferredTransitions.keyAt(i));
             }
+            for (int i = mInFlightTransactions.size() - 1; i >= 0; i--) {
+                // Cleanup any in-flight transactions to unblock the transition.
+                mInFlightTransactions.valueAt(i).meetAlternate("disposed(" + reason + ")");
+            }
             mOrganizer.asBinder().unlinkToDeath(this, 0 /* flags */);
         }
 
@@ -398,11 +411,6 @@
                 Slog.d(TAG, "Exception sending TaskFragmentTransaction", e);
                 return;
             }
-            onTransactionStarted(transaction.getTransactionToken());
-        }
-
-        /** Called when the transaction is sent to the organizer. */
-        void onTransactionStarted(@NonNull IBinder transactionToken) {
             if (!mWindowOrganizerController.getTransitionController().isCollecting()) {
                 return;
             }
@@ -410,9 +418,13 @@
                     .getCollectingTransitionId();
             ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
                     "Defer transition id=%d for TaskFragmentTransaction=%s", transitionId,
-                    transactionToken);
-            mDeferredTransitions.put(transactionToken, transitionId);
+                    transaction.getTransactionToken());
+            mDeferredTransitions.put(transaction.getTransactionToken(), transitionId);
             mWindowOrganizerController.getTransitionController().deferTransitionReady();
+            final Transition.ReadyCondition transactionApplied = new Transition.ReadyCondition(
+                    "task-fragment transaction", transaction);
+            mWindowOrganizerController.getTransitionController().waitFor(transactionApplied);
+            mInFlightTransactions.put(transaction.getTransactionToken(), transactionApplied);
         }
 
         /** Called when the transaction is finished. */
@@ -496,7 +508,7 @@
                 ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER,
                         "Unregister task fragment organizer=%s uid=%d pid=%d",
                         organizer.asBinder(), uid, pid);
-                removeOrganizer(organizer);
+                removeOrganizer(organizer, "unregistered");
             }
         } finally {
             Binder.restoreCallingIdentity(origId);
@@ -564,6 +576,11 @@
                     : null;
             if (state != null) {
                 state.onTransactionFinished(transactionToken);
+                final Transition.ReadyCondition condition =
+                        state.mInFlightTransactions.remove(transactionToken);
+                if (condition != null) {
+                    condition.meet();
+                }
             }
         }
     }
@@ -777,7 +794,8 @@
         return mTaskFragmentOrganizerState.containsKey(organizer.asBinder());
     }
 
-    private void removeOrganizer(@NonNull ITaskFragmentOrganizer organizer) {
+    private void removeOrganizer(@NonNull ITaskFragmentOrganizer organizer,
+            @NonNull String reason) {
         final TaskFragmentOrganizerState state = mTaskFragmentOrganizerState.get(
                 organizer.asBinder());
         if (state == null) {
@@ -788,7 +806,7 @@
         // event dispatch as result of surface placement.
         mPendingTaskFragmentEvents.remove(organizer.asBinder());
         // remove all of the children of the organized TaskFragment
-        state.dispose();
+        state.dispose(reason);
         mTaskFragmentOrganizerState.remove(organizer.asBinder());
     }
 
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 7d65c61..f6b4972 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -236,7 +236,8 @@
     private IRemoteCallback mClientAnimationFinishCallback = null;
 
     private @TransitionState int mState = STATE_PENDING;
-    private final ReadyTracker mReadyTracker = new ReadyTracker();
+    private final ReadyTrackerOld mReadyTrackerOld = new ReadyTrackerOld();
+    final ReadyTracker mReadyTracker = new ReadyTracker(this);
 
     private int mRecentsDisplayId = INVALID_DISPLAY;
 
@@ -656,7 +657,7 @@
             updateTransientFlags(info);
             mChanges.put(curr, info);
             if (isReadyGroup(curr)) {
-                mReadyTracker.addGroup(curr);
+                mReadyTrackerOld.addGroup(curr);
                 ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " Creating Ready-group for"
                                 + " Transition %d with root=%s", mSyncId, curr);
             }
@@ -887,13 +888,18 @@
      */
     void setReady(WindowContainer wc, boolean ready) {
         if (!isCollecting() || mSyncId < 0) return;
-        mReadyTracker.setReadyFrom(wc, ready);
+        mReadyTrackerOld.setReadyFrom(wc, ready);
         applyReady();
     }
 
     private void applyReady() {
         if (mState < STATE_STARTED) return;
-        final boolean ready = mReadyTracker.allReady();
+        final boolean ready;
+        if (mController.useFullReadyTracking()) {
+            ready = mReadyTracker.isReady();
+        } else {
+            ready = mReadyTrackerOld.allReady();
+        }
         ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
                 "Set transition ready=%b %d", ready, mSyncId);
         boolean changed = mSyncEngine.setReady(mSyncId, ready);
@@ -909,22 +915,22 @@
 
     /**
      * Sets all possible ready groups to ready.
-     * @see ReadyTracker#setAllReady.
+     * @see ReadyTrackerOld#setAllReady
      */
     void setAllReady() {
         if (!isCollecting() || mSyncId < 0) return;
-        mReadyTracker.setAllReady();
+        mReadyTrackerOld.setAllReady();
         applyReady();
     }
 
     @VisibleForTesting
     boolean allReady() {
-        return mReadyTracker.allReady();
+        return mReadyTrackerOld.allReady();
     }
 
     /** This transition has all of its expected participants. */
     boolean isPopulated() {
-        return mState >= STATE_STARTED && mReadyTracker.allReady();
+        return mState >= STATE_STARTED && mReadyTrackerOld.allReady();
     }
 
     /**
@@ -1438,6 +1444,13 @@
         ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Force Playing Transition: %d",
                 mSyncId);
         mForcePlaying = true;
+        // backwards since conditions are removed.
+        for (int i = mReadyTracker.mConditions.size() - 1; i >= 0; --i) {
+            mReadyTracker.mConditions.get(i).meetAlternate("play-now");
+        }
+        final ReadyCondition forcePlay = new ReadyCondition("force-play-now");
+        mReadyTracker.add(forcePlay);
+        forcePlay.meet();
         setAllReady();
         if (mState == STATE_COLLECTING) {
             start();
@@ -1483,6 +1496,21 @@
             return;
         }
 
+        if (mController.useFullReadyTracking()) {
+            if (mReadyTracker.mMet.isEmpty()) {
+                Slog.e(TAG, "#" + mSyncId + ": No conditions provided");
+            } else {
+                for (int i = 0; i < mReadyTracker.mConditions.size(); ++i) {
+                    Slog.e(TAG, "#" + mSyncId + ": unmet condition at ready: "
+                            + mReadyTracker.mConditions.get(i));
+                }
+            }
+            for (int i = 0; i < mReadyTracker.mMet.size(); ++i) {
+                ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "#%d: Met condition: %s",
+                        mSyncId, mReadyTracker.mMet.get(i));
+            }
+        }
+
         // Commit the visibility of visible activities before calculateTransitionInfo(), so the
         // TaskInfo can be visible. Also it needs to be done before moveToPlaying(), otherwise
         // ActivityRecord#canShowWindows() may reject to show its window. The visibility also
@@ -2797,7 +2825,7 @@
             // if an activity is pausing, it will call setReady(ar, false) and wait for the next
             // resumed activity. Then do not set to ready because the transition only contains
             // partial participants. Otherwise the transition may only handle HIDE and miss OPEN.
-            if (!mReadyTracker.mUsed) {
+            if (!mReadyTrackerOld.mUsed) {
                 setReady(dc, true);
             }
         }
@@ -3059,19 +3087,160 @@
      * {@link #continueTransitionReady}
      */
     void deferTransitionReady() {
-        ++mReadyTracker.mDeferReadyDepth;
+        ++mReadyTrackerOld.mDeferReadyDepth;
         // Make sure it wait until #continueTransitionReady() is called.
         mSyncEngine.setReady(mSyncId, false);
     }
 
     /** This undoes one call to {@link #deferTransitionReady}. */
     void continueTransitionReady() {
-        --mReadyTracker.mDeferReadyDepth;
+        --mReadyTrackerOld.mDeferReadyDepth;
         // Apply ready in case it is waiting for the previous defer call.
         applyReady();
     }
 
     /**
+     * Represents a condition that must be met before an associated transition can be considered
+     * ready.
+     *
+     * Expected usage is that a ReadyCondition is created and then attached to a transition's
+     * ReadyTracker via {@link ReadyTracker#add}. After that, it is expected to monitor the state
+     * of the system and when the condition it represents is met, it will call
+     * {@link ReadyTracker#meet}.
+     *
+     * This base class is a simple explicit, named condition. A caller will create/attach the
+     * condition and then explicitly call {@link #meet} on it (which internally calls
+     * {@link ReadyTracker#meet}.
+     *
+     * Example:
+     * <pre>
+     *     ReadyCondition myCondition = new ReadyCondition("my condition");
+     *     transitionController.waitFor(myCondition);
+     *     ... Some operations ...
+     *     myCondition.meet();
+     * </pre>
+     */
+    static class ReadyCondition {
+        final String mName;
+
+        /** Just used for debugging */
+        final Object mDebugTarget;
+        ReadyTracker mTracker;
+        boolean mMet = false;
+
+        /** If set (non-null), then this is met by another reason besides state (eg. timeout). */
+        String mAlternate = null;
+
+        ReadyCondition(@NonNull String name) {
+            mName = name;
+            mDebugTarget = null;
+        }
+
+        ReadyCondition(@NonNull String name, @Nullable Object debugTarget) {
+            mName = name;
+            mDebugTarget = debugTarget;
+        }
+
+        protected String getDebugRep() {
+            if (mDebugTarget != null) {
+                return mName + ":" + mDebugTarget;
+            }
+            return mName;
+        }
+
+        @Override
+        public String toString() {
+            return "{" + getDebugRep() + (mAlternate != null ? " (" + mAlternate + ")" : "") + "}";
+        }
+
+        /**
+         * Instructs this condition to start tracking system state to detect when this is met.
+         * Don't call this directly; it is called when this object is attached to a transition's
+         * ready-tracker.
+         */
+        void startTracking() {
+        }
+
+        /**
+         * Immediately consider this condition met by an alternative reason (one which doesn't
+         * match the normal intent of this condition -- eg. a timeout).
+         */
+        void meetAlternate(@NonNull String reason) {
+            if (mMet) return;
+            mAlternate = reason;
+            meet();
+        }
+
+        /** Immediately consider this condition met. */
+        void meet() {
+            if (mMet) return;
+            if (mTracker == null) {
+                throw new IllegalStateException("Can't meet a condition before it is waited on");
+            }
+            mTracker.meet(this);
+        }
+    }
+
+    static class ReadyTracker {
+        /**
+         * Used as a place-holder in situations where the transition system isn't active (such as
+         * early-boot, mid shell crash/recovery, or when using legacy).
+         */
+        static final ReadyTracker NULL_TRACKER = new ReadyTracker(null);
+
+        private final Transition mTransition;
+
+        /** List of conditions that are still being waited on. */
+        final ArrayList<ReadyCondition> mConditions = new ArrayList<>();
+
+        /** List of already-met conditions. Fully-qualified for debugging. */
+        final ArrayList<ReadyCondition> mMet = new ArrayList<>();
+
+        ReadyTracker(Transition transition) {
+            mTransition = transition;
+        }
+
+        void add(@NonNull ReadyCondition condition) {
+            if (mTransition == null || !mTransition.mController.useFullReadyTracking()) {
+                condition.mTracker = NULL_TRACKER;
+                return;
+            }
+            mConditions.add(condition);
+            condition.mTracker = this;
+            ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " Add condition %s for #%d",
+                    condition, mTransition.mSyncId);
+            condition.startTracking();
+        }
+
+        void meet(@NonNull ReadyCondition condition) {
+            if (mTransition == null || !mTransition.mController.useFullReadyTracking()) return;
+            if (mTransition.mState >= STATE_PLAYING) {
+                Slog.w(TAG, "#%d: Condition met too late, already in state=" + mTransition.mState
+                        + ": " + condition);
+                return;
+            }
+            if (!mConditions.remove(condition)) {
+                if (mMet.contains(condition)) {
+                    throw new IllegalStateException("Can't meet the same condition more than once: "
+                            + condition + " #" + mTransition.mSyncId);
+                } else {
+                    throw new IllegalArgumentException("Can't meet a condition that isn't being "
+                            + "waited on: " + condition + " in #" + mTransition.mSyncId);
+                }
+            }
+            condition.mMet = true;
+            ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " Met condition %s for #%d (%d"
+                    + " left)", condition, mTransition.mSyncId, mConditions.size());
+            mMet.add(condition);
+            mTransition.applyReady();
+        }
+
+        boolean isReady() {
+            return mConditions.isEmpty() && !mMet.isEmpty();
+        }
+    }
+
+    /**
      * The transition sync mechanism has 2 parts:
      *   1. Whether all WM operations for a particular transition are "ready" (eg. did the app
      *      launch or stop or get a new configuration?).
@@ -3084,7 +3253,7 @@
      * of readiness across the multiple groups. Currently, we assume that each display is a group
      * since that is how it has been until now.
      */
-    private static class ReadyTracker {
+    private static class ReadyTrackerOld {
         private final ArrayMap<WindowContainer, Boolean> mReadyGroups = new ArrayMap<>();
 
         /**
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 8ac21e4..28c24c8 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -62,6 +62,7 @@
 import com.android.internal.protolog.ProtoLogGroup;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.server.FgThread;
+import com.android.window.flags.Flags;
 
 import java.util.ArrayList;
 import java.util.function.Consumer;
@@ -131,6 +132,7 @@
     TransitionTracer mTransitionTracer;
 
     private SystemPerformanceHinter mSystemPerformanceHinter;
+    private boolean mFullReadyTracking = false;
 
     private final ArrayList<WindowManagerInternal.AppTransitionListener> mLegacyListeners =
             new ArrayList<>();
@@ -281,6 +283,7 @@
         registerLegacyListener(wms.mActivityManagerAppTransitionNotifier);
         setSyncEngine(wms.mSyncEngine);
         setSystemPerformanceHinter(wms.mSystemPerformanceHinter);
+        mFullReadyTracking = Flags.transitReadyTracking();
     }
 
     @VisibleForTesting
@@ -397,6 +400,14 @@
         return isShellTransitionsEnabled() && SHELL_TRANSITIONS_ROTATION;
     }
 
+    boolean useFullReadyTracking() {
+        return mFullReadyTracking;
+    }
+
+    void setFullReadyTrackingForTest(boolean enabled) {
+        mFullReadyTracking = enabled;
+    }
+
     /**
      * @return {@code true} if transition is actively collecting changes. This is {@code false}
      * once a transition is playing
@@ -1475,6 +1486,19 @@
         applySync.accept(false /* deferred */);
     }
 
+    /**
+     * Instructs the collecting transition to wait until `condition` is met before it is
+     * considered ready.
+     */
+    void waitFor(@NonNull Transition.ReadyCondition condition) {
+        if (mCollectingTransition == null) {
+            Slog.e(TAG, "No collecting transition available to wait for " + condition);
+            condition.mTracker = Transition.ReadyTracker.NULL_TRACKER;
+            return;
+        }
+        mCollectingTransition.mReadyTracker.add(condition);
+    }
+
     interface OnStartCollect {
         void onCollectStarted(boolean deferred);
     }
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 4a0f44b..8f884d2f 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -2706,6 +2706,10 @@
         return mLastLayer;
     }
 
+    SurfaceControl getLastRelativeLayer() {
+        return mLastRelativeToLayer;
+    }
+
     protected void setRelativeLayer(Transaction t, SurfaceControl relativeTo, int layer) {
         if (mSurfaceFreezer.hasLeash()) {
             // When the freezer has created animation leash parent for the window, set the layer
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 5ed6caf..eb60aab 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -305,9 +305,12 @@
                     // This is a direct call from shell, so the entire transition lifecycle is
                     // contained in the provided transaction if provided. Thus, we can setReady
                     // immediately after apply.
+                    final Transition.ReadyCondition wctApplied =
+                            new Transition.ReadyCondition("start WCT applied");
                     final boolean needsSetReady = t != null;
                     final Transition nextTransition = new Transition(type, 0 /* flags */,
                             mTransitionController, mService.mWindowManager.mSyncEngine);
+                    nextTransition.mReadyTracker.add(wctApplied);
                     nextTransition.calcParallelCollectType(wct);
                     mTransitionController.startCollectOrQueue(nextTransition,
                             (deferred) -> {
@@ -315,6 +318,7 @@
                                 nextTransition.mLogger.mStartWCT = wct;
                                 applyTransaction(wct, -1 /* syncId */, nextTransition, caller,
                                         deferred);
+                                wctApplied.meet();
                                 if (needsSetReady) {
                                     // TODO(b/294925498): Remove this once we have accurate ready
                                     //                    tracking.
@@ -329,6 +333,15 @@
                             });
                     return nextTransition.getToken();
                 }
+                // Currently, application of wct can span multiple looper loops (ie.
+                // waitAsyncStart), so add a condition to ensure that it finishes applying.
+                final Transition.ReadyCondition wctApplied;
+                if (t != null) {
+                    wctApplied = new Transition.ReadyCondition("start WCT applied");
+                    transition.mReadyTracker.add(wctApplied);
+                } else {
+                    wctApplied = null;
+                }
                 // The transition already started collecting before sending a request to shell,
                 // so just start here.
                 if (!transition.isCollecting() && !transition.isForcePlaying()) {
@@ -336,6 +349,9 @@
                             + " means Shell took too long to respond to a request. WM State may be"
                             + " incorrect now, please file a bug");
                     applyTransaction(wct, -1 /*syncId*/, null /*transition*/, caller);
+                    if (wctApplied != null) {
+                        wctApplied.meet();
+                    }
                     return transition.getToken();
                 }
                 transition.mLogger.mStartWCT = wct;
@@ -344,11 +360,17 @@
                         synchronized (mService.mGlobalLock) {
                             transition.start();
                             applyTransaction(wct, -1 /* syncId */, transition, caller);
+                            if (wctApplied != null) {
+                                wctApplied.meet();
+                            }
                         }
                     });
                 } else {
                     transition.start();
                     applyTransaction(wct, -1 /* syncId */, transition, caller);
+                    if (wctApplied != null) {
+                        wctApplied.meet();
+                    }
                 }
                 // Since the transition is already provided, it means WMCore is determining the
                 // "readiness lifecycle" outside the provided transaction, so don't set ready here.
@@ -1159,9 +1181,13 @@
             }
             case HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP: {
                 final WindowContainer container = WindowContainer.fromBinder(hop.getContainer());
-                if (container == null || container.asDisplayArea() == null
-                        || !container.isAttached()) {
-                    Slog.e(TAG, "Attempt to operate on unknown or detached display area: "
+                if (container == null || !container.isAttached()) {
+                    Slog.e(TAG, "Attempt to operate on unknown or detached container: "
+                            + container);
+                    break;
+                }
+                if (container.asTask() == null && container.asDisplayArea() == null) {
+                    Slog.e(TAG, "Cannot set always-on-top on non-task or non-display area: "
                             + container);
                     break;
                 }
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index e769a27..a74a707 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -42,7 +42,6 @@
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.ActivityTaskManagerService.INSTRUMENTATION_KEY_DISPATCHING_TIMEOUT_MILLIS;
 import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_NONE;
-import static com.android.server.wm.BackgroundActivityStartController.BAL_BLOCK;
 import static com.android.server.wm.WindowManagerService.MY_PID;
 
 import static java.util.Objects.requireNonNull;
@@ -631,22 +630,23 @@
      */
     @HotPath(caller = HotPath.START_SERVICE)
     public boolean areBackgroundFgsStartsAllowed() {
-        return areBackgroundActivityStartsAllowed(mAtm.getBalAppSwitchesState(),
-                true /* isCheckingForFgsStart */) != BAL_BLOCK;
+        return areBackgroundActivityStartsAllowed(
+                mAtm.getBalAppSwitchesState(),
+                true /* isCheckingForFgsStart */).allows();
     }
 
-    @BackgroundActivityStartController.BalCode
-    int areBackgroundActivityStartsAllowed(int appSwitchState) {
-        return areBackgroundActivityStartsAllowed(appSwitchState,
+    BackgroundActivityStartController.BalVerdict areBackgroundActivityStartsAllowed(
+            int appSwitchState) {
+        return areBackgroundActivityStartsAllowed(
+                appSwitchState,
                 false /* isCheckingForFgsStart */);
     }
 
-    @BackgroundActivityStartController.BalCode
-    private int areBackgroundActivityStartsAllowed(int appSwitchState,
-            boolean isCheckingForFgsStart) {
-        return mBgLaunchController.areBackgroundActivityStartsAllowed(mPid, mUid, mInfo.packageName,
-                appSwitchState, isCheckingForFgsStart, hasActivityInVisibleTask(),
-                mInstrumentingWithBackgroundActivityStartPrivileges,
+    private BackgroundActivityStartController.BalVerdict areBackgroundActivityStartsAllowed(
+            int appSwitchState, boolean isCheckingForFgsStart) {
+        return mBgLaunchController.areBackgroundActivityStartsAllowed(mPid, mUid,
+                mInfo.packageName, appSwitchState, isCheckingForFgsStart,
+                hasActivityInVisibleTask(), mInstrumentingWithBackgroundActivityStartPrivileges,
                 mAtm.getLastStopAppSwitchesTime(),
                 mLastActivityLaunchTime, mLastActivityFinishTime);
     }
diff --git a/services/core/jni/com_android_server_power_stats_CpuPowerStatsCollector.cpp b/services/core/jni/com_android_server_power_stats_CpuPowerStatsCollector.cpp
index a6084ea..cac13eb 100644
--- a/services/core/jni/com_android_server_power_stats_CpuPowerStatsCollector.cpp
+++ b/services/core/jni/com_android_server_power_stats_CpuPowerStatsCollector.cpp
@@ -34,9 +34,12 @@
 
 static constexpr uint64_t NSEC_PER_MSEC = 1000000;
 
-static int extractUidStats(JNIEnv *env, std::vector<std::vector<uint64_t>> &times,
-                           ScopedIntArrayRO &scopedScalingStepToPowerBracketMap,
-                           jlongArray tempForUidStats);
+static int flatten(JNIEnv *env, const std::vector<std::vector<uint64_t>> &times,
+                   jlongArray outArray);
+
+static int combineByBracket(JNIEnv *env, const std::vector<std::vector<uint64_t>> &times,
+                            ScopedIntArrayRO &scopedScalingStepToPowerBracketMap,
+                            jlongArray outBrackets);
 
 static bool initialized = false;
 static jclass class_KernelCpuStatsCallback;
@@ -62,25 +65,43 @@
     return OK;
 }
 
+static jboolean nativeIsSupportedFeature(JNIEnv *env) {
+    if (!android::bpf::startTrackingUidTimes()) {
+        return false;
+    }
+    auto totalByScalingStep = android::bpf::getTotalCpuFreqTimes();
+    return totalByScalingStep.has_value();
+}
+
 static jlong nativeReadCpuStats(JNIEnv *env, [[maybe_unused]] jobject zis, jobject callback,
                                 jintArray scalingStepToPowerBracketMap,
-                                jlong lastUpdateTimestampNanos, jlongArray tempForUidStats) {
+                                jlong lastUpdateTimestampNanos, jlongArray cpuTimeByScalingStep,
+                                jlongArray tempForUidStats) {
+    ScopedIntArrayRO scopedScalingStepToPowerBracketMap(env, scalingStepToPowerBracketMap);
+
     if (!initialized) {
         if (init(env) == EXCEPTION) {
             return 0L;
         }
     }
 
+    auto totalByScalingStep = android::bpf::getTotalCpuFreqTimes();
+    if (!totalByScalingStep) {
+        jniThrowExceptionFmt(env, "java/lang/RuntimeException", "Unsupported kernel feature");
+        return EXCEPTION;
+    }
+
+    if (flatten(env, *totalByScalingStep, cpuTimeByScalingStep) == EXCEPTION) {
+        return 0L;
+    }
+
     uint64_t newLastUpdate = lastUpdateTimestampNanos;
     auto data = android::bpf::getUidsUpdatedCpuFreqTimes(&newLastUpdate);
     if (!data.has_value()) return lastUpdateTimestampNanos;
 
-    ScopedIntArrayRO scopedScalingStepToPowerBracketMap(env, scalingStepToPowerBracketMap);
-
     for (auto &[uid, times] : *data) {
-        int status =
-                extractUidStats(env, times, scopedScalingStepToPowerBracketMap, tempForUidStats);
-        if (status == EXCEPTION) {
+        if (combineByBracket(env, times, scopedScalingStepToPowerBracketMap, tempForUidStats) ==
+            EXCEPTION) {
             return 0L;
         }
         env->CallVoidMethod(callback, method_KernelCpuStatsCallback_processUidStats, (jint)uid,
@@ -89,13 +110,34 @@
     return newLastUpdate;
 }
 
-static int extractUidStats(JNIEnv *env, std::vector<std::vector<uint64_t>> &times,
-                           ScopedIntArrayRO &scopedScalingStepToPowerBracketMap,
-                           jlongArray tempForUidStats) {
-    ScopedLongArrayRW scopedTempForStats(env, tempForUidStats);
-    uint64_t *arrayForStats = reinterpret_cast<uint64_t *>(scopedTempForStats.get());
-    const uint8_t statsSize = scopedTempForStats.size();
-    memset(arrayForStats, 0, statsSize * sizeof(uint64_t));
+static int flatten(JNIEnv *env, const std::vector<std::vector<uint64_t>> &times,
+                   jlongArray outArray) {
+    ScopedLongArrayRW scopedOutArray(env, outArray);
+    const uint8_t scalingStepCount = scopedOutArray.size();
+    uint64_t *out = reinterpret_cast<uint64_t *>(scopedOutArray.get());
+    uint32_t scalingStep = 0;
+    for (const auto &subVec : times) {
+        for (uint32_t i = 0; i < subVec.size(); ++i) {
+            if (scalingStep >= scalingStepCount) {
+                jniThrowExceptionFmt(env, "java/lang/IndexOutOfBoundsException",
+                                     "Array is too short, size=%u, scalingStep=%u",
+                                     scalingStepCount, scalingStep);
+                return EXCEPTION;
+            }
+            out[scalingStep] = subVec[i] / NSEC_PER_MSEC;
+            scalingStep++;
+        }
+    }
+    return OK;
+}
+
+static int combineByBracket(JNIEnv *env, const std::vector<std::vector<uint64_t>> &times,
+                            ScopedIntArrayRO &scopedScalingStepToPowerBracketMap,
+                            jlongArray outBrackets) {
+    ScopedLongArrayRW scopedOutBrackets(env, outBrackets);
+    uint64_t *brackets = reinterpret_cast<uint64_t *>(scopedOutBrackets.get());
+    const uint8_t statsSize = scopedOutBrackets.size();
+    memset(brackets, 0, statsSize * sizeof(uint64_t));
     const uint8_t scalingStepCount = scopedScalingStepToPowerBracketMap.size();
 
     uint32_t scalingStep = 0;
@@ -108,14 +150,14 @@
                                      scalingStepCount, scalingStep);
                 return EXCEPTION;
             }
-            uint32_t bucket = scopedScalingStepToPowerBracketMap[scalingStep];
-            if (bucket >= statsSize) {
+            uint32_t bracket = scopedScalingStepToPowerBracketMap[scalingStep];
+            if (bracket >= statsSize) {
                 jniThrowExceptionFmt(env, "java/lang/IndexOutOfBoundsException",
-                                     "UidStats array is too short, length=%u, bucket[%u]=%u",
-                                     statsSize, scalingStep, bucket);
+                                     "Bracket array is too short, length=%u, bracket[%u]=%u",
+                                     statsSize, scalingStep, bracket);
                 return EXCEPTION;
             }
-            arrayForStats[bucket] += subVec[i] / NSEC_PER_MSEC;
+            brackets[bracket] += subVec[i] / NSEC_PER_MSEC;
             scalingStep++;
         }
     }
@@ -123,7 +165,8 @@
 }
 
 static const JNINativeMethod method_table[] = {
-        {"nativeReadCpuStats", "(L" JAVA_CLASS_KERNEL_CPU_STATS_CALLBACK ";[IJ[J)J",
+        {"nativeIsSupportedFeature", "()Z", (void *)nativeIsSupportedFeature},
+        {"nativeReadCpuStats", "(L" JAVA_CLASS_KERNEL_CPU_STATS_CALLBACK ";[IJ[J[J)J",
          (void *)nativeReadCpuStats},
 };
 
diff --git a/services/devicepolicy/Android.bp b/services/devicepolicy/Android.bp
index 41706f0..8dfa685 100644
--- a/services/devicepolicy/Android.bp
+++ b/services/devicepolicy/Android.bp
@@ -23,8 +23,6 @@
         "services.core",
         "app-compat-annotations",
         "service-permission.stubs.system_server",
-    ],
-    static_libs: [
         "device_policy_aconfig_flags_lib",
     ],
 }
diff --git a/services/smartspace/java/com/android/server/smartspace/SmartspaceManagerService.java b/services/smartspace/java/com/android/server/smartspace/SmartspaceManagerService.java
index ca57f51..8b5d7f0 100644
--- a/services/smartspace/java/com/android/server/smartspace/SmartspaceManagerService.java
+++ b/services/smartspace/java/com/android/server/smartspace/SmartspaceManagerService.java
@@ -31,6 +31,7 @@
 import android.app.smartspace.SmartspaceConfig;
 import android.app.smartspace.SmartspaceSessionId;
 import android.app.smartspace.SmartspaceTargetEvent;
+import android.app.smartspace.flags.Flags;
 import android.content.Context;
 import android.os.Binder;
 import android.os.IBinder;
@@ -165,7 +166,8 @@
             }
             Context ctx = getContext();
             if (!(ctx.checkCallingPermission(MANAGE_SMARTSPACE) == PERMISSION_GRANTED
-                    || ctx.checkCallingPermission(ACCESS_SMARTSPACE) == PERMISSION_GRANTED
+                    || (Flags.accessSmartspace()
+                    && ctx.checkCallingPermission(ACCESS_SMARTSPACE) == PERMISSION_GRANTED)
                     || mServiceNameResolver.isTemporary(userId)
                     || mActivityTaskManagerInternal.isCallerRecents(Binder.getCallingUid()))) {
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
index bded9b4..809a0e8 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
@@ -919,6 +919,7 @@
             assertFalse(controller.isSatisfied(latePrefetchUnknownUp, net, caps, mConstants));
             mSetFlagsRule.disableFlags(FLAG_RELAX_PREFETCH_CONNECTIVITY_CONSTRAINT_ONLY_ON_CHARGER);
             when(mService.isBatteryCharging()).thenReturn(false);
+            when(mService.isBatteryNotLow()).thenReturn(false);
 
             when(mNetPolicyManagerInternal.getSubscriptionOpportunisticQuota(
                     any(), eq(NetworkPolicyManagerInternal.QUOTA_TYPE_JOBS)))
@@ -938,6 +939,12 @@
             assertFalse(controller.isSatisfied(latePrefetchUnknownUp, net, caps, mConstants));
 
             when(mService.isBatteryCharging()).thenReturn(true);
+            assertFalse(controller.isSatisfied(latePrefetch, net, caps, mConstants));
+            // Only relax restrictions when we at least know the estimated download bytes.
+            assertFalse(controller.isSatisfied(latePrefetchUnknownDown, net, caps, mConstants));
+            assertFalse(controller.isSatisfied(latePrefetchUnknownUp, net, caps, mConstants));
+
+            when(mService.isBatteryNotLow()).thenReturn(true);
             assertTrue(controller.isSatisfied(latePrefetch, net, caps, mConstants));
             // Only relax restrictions when we at least know the estimated download bytes.
             assertFalse(controller.isSatisfied(latePrefetchUnknownDown, net, caps, mConstants));
diff --git a/services/tests/powerstatstests/res/xml/power_profile_test.xml b/services/tests/powerstatstests/res/xml/power_profile_test.xml
new file mode 100644
index 0000000..ecd8861
--- /dev/null
+++ b/services/tests/powerstatstests/res/xml/power_profile_test.xml
@@ -0,0 +1,101 @@
+<?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.
+  -->
+
+<device name="Android">
+    <!-- This is the battery capacity in mAh -->
+    <item name="battery.capacity">3000</item>
+
+    <!-- Power consumption when CPU is suspended -->
+    <item name="cpu.suspend">5</item>
+    <!-- Additional power consumption when CPU is in a kernel idle loop -->
+    <item name="cpu.idle">1.11</item>
+    <!-- Additional power consumption by CPU excluding cluster and core when  running -->
+    <item name="cpu.active">2.55</item>
+
+    <!-- Additional power consumption of CPU policy0 itself when running on related cores -->
+    <item name="cpu.scaling_policy_power.policy0">2.11</item>
+    <!-- Additional power consumption of CPU policy4 itself when running on related cores -->
+    <item name="cpu.scaling_policy_power.policy4">2.22</item>
+
+    <!-- Additional power used by a CPU related to policy3 when running at different
+     speeds. -->
+    <array name="cpu.scaling_step_power.policy0">
+        <value>10</value> <!-- 300 MHz CPU speed -->
+        <value>20</value> <!-- 1000 MHz CPU speed -->
+        <value>30</value> <!-- 1900 MHz CPU speed -->
+    </array>
+    <!-- Additional power used by a CPU related to policy3 when running at different
+         speeds. -->
+    <array name="cpu.scaling_step_power.policy4">
+        <value>25</value> <!-- 300 MHz CPU speed -->
+        <value>35</value> <!-- 1000 MHz CPU speed -->
+        <value>50</value> <!-- 2500 MHz CPU speed -->
+        <value>60</value> <!-- 3000 MHz CPU speed -->
+    </array>
+
+    <!-- Power used by display unit in ambient display mode, including back lighting-->
+    <item name="ambient.on">0.5</item>
+    <!-- Additional power used when screen is turned on at minimum brightness -->
+    <item name="screen.on">100</item>
+    <!-- Additional power used when screen is at maximum brightness, compared to
+         screen at minimum brightness -->
+    <item name="screen.full">800</item>
+
+    <!-- Average power used by the camera flash module when on -->
+    <item name="camera.flashlight">500</item>
+    <!-- Average power use by the camera subsystem for a typical camera
+         application. Intended as a rough estimate for an application running a
+         preview and capturing approximately 10 full-resolution pictures per
+         minute. -->
+    <item name="camera.avg">600</item>
+
+    <!-- Additional power used by the audio hardware, probably due to DSP -->
+    <item name="audio">100.0</item>
+
+    <!-- Additional power used by the video hardware, probably due to DSP -->
+    <item name="video">150.0</item> <!-- ~50mA -->
+
+    <!-- Additional power used when GPS is acquiring a signal -->
+    <item name="gps.on">10</item>
+
+    <!-- Additional power used when cellular radio is transmitting/receiving -->
+    <item name="radio.active">60</item>
+    <!-- Additional power used when cellular radio is paging the tower -->
+    <item name="radio.scanning">3</item>
+    <!-- Additional power used when the cellular radio is on. Multi-value entry,
+         one per signal strength (no signal, weak, moderate, strong) -->
+    <array name="radio.on"> <!-- Strength 0 to BINS-1 -->
+        <value>6</value>       <!-- none -->
+        <value>5</value>       <!-- poor -->
+        <value>4</value>       <!-- moderate -->
+        <value>3</value>       <!-- good -->
+        <value>3</value>       <!-- great -->
+    </array>
+
+    <!-- Cellular modem related values. These constants are deprecated, but still supported and
+         need to be tested -->
+    <item name="modem.controller.sleep">123</item>
+    <item name="modem.controller.idle">456</item>
+    <item name="modem.controller.rx">789</item>
+    <array name="modem.controller.tx"> <!-- Strength 0 to 4 -->
+        <value>10</value>
+        <value>20</value>
+        <value>30</value>
+        <value>40</value>
+        <value>50</value>
+    </array>
+</device>
\ No newline at end of file
diff --git a/core/tests/coretests/res/xml/power_profile_test_power_brackets.xml b/services/tests/powerstatstests/res/xml/power_profile_test_power_brackets.xml
similarity index 100%
rename from core/tests/coretests/res/xml/power_profile_test_power_brackets.xml
rename to services/tests/powerstatstests/res/xml/power_profile_test_power_brackets.xml
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/AggregatePowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/AggregatePowerStatsProcessorTest.java
new file mode 100644
index 0000000..48e2dd7
--- /dev/null
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/AggregatePowerStatsProcessorTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.power.stats;
+
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.STATE_POWER;
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.STATE_PROCESS_STATE;
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.STATE_SCREEN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.BatteryConsumer;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.os.MultiStateStats;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class AggregatePowerStatsProcessorTest {
+
+    @Test
+    public void createPowerEstimationPlan_allDeviceStatesPresentInUidStates() {
+        AggregatedPowerStatsConfig.PowerComponent config =
+                new AggregatedPowerStatsConfig.PowerComponent(BatteryConsumer.POWER_COMPONENT_ANY)
+                        .trackDeviceStates(STATE_POWER, STATE_SCREEN)
+                        .trackUidStates(STATE_POWER, STATE_SCREEN, STATE_PROCESS_STATE);
+
+        AggregatedPowerStatsProcessor.PowerEstimationPlan plan =
+                new AggregatedPowerStatsProcessor.PowerEstimationPlan(config);
+        assertThat(deviceStateEstimatesToStrings(plan))
+                .containsExactly("[0, 0]", "[0, 1]", "[1, 0]", "[1, 1]");
+        assertThat(combinedDeviceStatsToStrings(plan))
+                .containsExactly("[[0, 0]]", "[[0, 1]]", "[[1, 0]]", "[[1, 1]]");
+        assertThat(uidStateEstimatesToStrings(plan, config))
+                .containsExactly(
+                        "[[0, 0]]: [ps]: [[0, 0, 0], [0, 0, 1], [0, 0, 2], [0, 0, 3], [0, 0, 4]]",
+                        "[[0, 1]]: [ps]: [[0, 1, 0], [0, 1, 1], [0, 1, 2], [0, 1, 3], [0, 1, 4]]",
+                        "[[1, 0]]: [ps]: [[1, 0, 0], [1, 0, 1], [1, 0, 2], [1, 0, 3], [1, 0, 4]]",
+                        "[[1, 1]]: [ps]: [[1, 1, 0], [1, 1, 1], [1, 1, 2], [1, 1, 3], [1, 1, 4]]");
+    }
+
+    @Test
+    public void createPowerEstimationPlan_combineDeviceStats() {
+        AggregatedPowerStatsConfig.PowerComponent config =
+                new AggregatedPowerStatsConfig.PowerComponent(BatteryConsumer.POWER_COMPONENT_ANY)
+                        .trackDeviceStates(STATE_POWER, STATE_SCREEN)
+                        .trackUidStates(STATE_POWER, STATE_PROCESS_STATE);
+
+        AggregatedPowerStatsProcessor.PowerEstimationPlan plan =
+                new AggregatedPowerStatsProcessor.PowerEstimationPlan(config);
+
+        assertThat(deviceStateEstimatesToStrings(plan))
+                .containsExactly("[0, 0]", "[0, 1]", "[1, 0]", "[1, 1]");
+        assertThat(combinedDeviceStatsToStrings(plan))
+                .containsExactly(
+                        "[[0, 0], [0, 1]]",
+                        "[[1, 0], [1, 1]]");
+        assertThat(uidStateEstimatesToStrings(plan, config))
+                .containsExactly(
+                        "[[0, 0], [0, 1]]: [ps]: [[0, 0], [0, 1], [0, 2], [0, 3], [0, 4]]",
+                        "[[1, 0], [1, 1]]: [ps]: [[1, 0], [1, 1], [1, 2], [1, 3], [1, 4]]");
+    }
+
+    private static List<String> deviceStateEstimatesToStrings(
+            AggregatedPowerStatsProcessor.PowerEstimationPlan plan) {
+        return plan.deviceStateEstimations.stream()
+                .map(dse -> dse.stateValues).map(Arrays::toString).toList();
+    }
+
+    private static List<String> combinedDeviceStatsToStrings(
+            AggregatedPowerStatsProcessor.PowerEstimationPlan plan) {
+        return plan.combinedDeviceStateEstimations.stream()
+                .map(cds -> cds.deviceStateEstimations)
+                .map(dses -> dses.stream()
+                        .map(dse -> dse.stateValues).map(Arrays::toString).toList())
+                .map(Object::toString)
+                .toList();
+    }
+
+    private static List<String> uidStateEstimatesToStrings(
+            AggregatedPowerStatsProcessor.PowerEstimationPlan plan,
+            AggregatedPowerStatsConfig.PowerComponent config) {
+        MultiStateStats.States[] uidStateConfig = config.getUidStateConfig();
+        return plan.uidStateEstimates.stream()
+                .map(use ->
+                        use.combinedDeviceStateEstimate.deviceStateEstimations.stream()
+                                .map(dse -> dse.stateValues).map(Arrays::toString).toList()
+                        + ": "
+                        + Arrays.stream(use.states)
+                                .filter(Objects::nonNull)
+                                .map(MultiStateStats.States::getName).toList()
+                        + ": "
+                        + use.proportionalEstimates.stream()
+                                .map(pe -> trackedStatesToString(uidStateConfig, pe.stateValues))
+                                .toList())
+                .toList();
+    }
+
+    private static Object trackedStatesToString(MultiStateStats.States[] states,
+            int[] stateValues) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("[");
+        boolean first = true;
+        for (int i = 0; i < states.length; i++) {
+            if (!states[i].isTracked()) {
+                continue;
+            }
+
+            if (!first) {
+                sb.append(", ");
+            }
+            first = false;
+            sb.append(stateValues[i]);
+        }
+        sb.append("]");
+        return sb.toString();
+    }
+}
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
index 3579fce..0b10954 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
@@ -142,6 +142,22 @@
         return this;
     }
 
+    /**
+     * Mocks the CPU bracket count
+     */
+    public BatteryUsageStatsRule setCpuPowerBracketCount(int count) {
+        when(mPowerProfile.getCpuPowerBracketCount()).thenReturn(count);
+        return this;
+    }
+
+    /**
+     * Mocks the CPU bracket for the given CPU scaling policy and step
+     */
+    public BatteryUsageStatsRule setCpuPowerBracket(int policy, int step, int bracket) {
+        when(mPowerProfile.getCpuPowerBracketForScalingStep(policy, step)).thenReturn(bracket);
+        return this;
+    }
+
     public BatteryUsageStatsRule setAveragePowerForOrdinal(String group, int ordinal,
             double value) {
         when(mPowerProfile.getAveragePowerForOrdinal(group, ordinal)).thenReturn(value);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuAggregatedPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuAggregatedPowerStatsProcessorTest.java
new file mode 100644
index 0000000..79084cc
--- /dev/null
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuAggregatedPowerStatsProcessorTest.java
@@ -0,0 +1,318 @@
+/*
+ * 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.power.stats;
+
+import static android.os.BatteryConsumer.PROCESS_STATE_BACKGROUND;
+import static android.os.BatteryConsumer.PROCESS_STATE_CACHED;
+import static android.os.BatteryConsumer.PROCESS_STATE_FOREGROUND;
+
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.POWER_STATE_BATTERY;
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.POWER_STATE_OTHER;
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.SCREEN_STATE_ON;
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.SCREEN_STATE_OTHER;
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.STATE_POWER;
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.STATE_PROCESS_STATE;
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.STATE_SCREEN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+
+import android.os.BatteryConsumer;
+import android.os.PersistableBundle;
+import android.util.LongArray;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.os.MultiStateStats;
+import com.android.internal.os.PowerProfile;
+import com.android.internal.os.PowerStats;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CpuAggregatedPowerStatsProcessorTest {
+    @Rule
+    public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
+            .setAveragePower(PowerProfile.POWER_CPU_ACTIVE, 720)
+            .setCpuScalingPolicy(0, new int[]{0, 1}, new int[]{100, 200})
+            .setCpuScalingPolicy(2, new int[]{2, 3}, new int[]{300})
+            .setAveragePowerForCpuScalingPolicy(0, 360)
+            .setAveragePowerForCpuScalingPolicy(2, 480)
+            .setAveragePowerForCpuScalingStep(0, 0, 300)
+            .setAveragePowerForCpuScalingStep(0, 1, 400)
+            .setAveragePowerForCpuScalingStep(2, 0, 500)
+            .setCpuPowerBracketCount(3)
+            .setCpuPowerBracket(0, 0, 0)
+            .setCpuPowerBracket(0, 1, 1)
+            .setCpuPowerBracket(2, 0, 2);
+
+    private AggregatedPowerStatsConfig.PowerComponent mConfig;
+    private CpuAggregatedPowerStatsProcessor mProcessor;
+    private MockPowerComponentAggregatedPowerStats mStats;
+
+    @Before
+    public void setup() {
+        mConfig = new AggregatedPowerStatsConfig.PowerComponent(BatteryConsumer.POWER_COMPONENT_CPU)
+                .trackDeviceStates(STATE_POWER, STATE_SCREEN)
+                .trackUidStates(STATE_POWER, STATE_SCREEN, STATE_PROCESS_STATE);
+
+        mProcessor = new CpuAggregatedPowerStatsProcessor(
+                mStatsRule.getPowerProfile(), mStatsRule.getCpuScalingPolicies());
+    }
+
+    @Test
+    public void powerProfileModel() {
+        mStats = new MockPowerComponentAggregatedPowerStats(mConfig, false);
+        mStats.setDeviceStats(
+                states(POWER_STATE_BATTERY, SCREEN_STATE_ON),
+                concat(
+                        values(3500, 4500, 3000),   // scaling steps
+                        values(2000, 1000),         // clusters
+                        values(5000)),              // uptime
+                3.113732);
+        mStats.setDeviceStats(
+                states(POWER_STATE_OTHER, SCREEN_STATE_ON),
+                concat(
+                        values(6000, 6500, 4000),
+                        values(5000, 3000),
+                        values(7000)),
+                4.607245);
+        mStats.setDeviceStats(
+                states(POWER_STATE_OTHER, SCREEN_STATE_OTHER),
+                concat(
+                        values(9000, 10000, 7000),
+                        values(8000, 6000),
+                        values(20000)),
+                7.331799);
+        mStats.setUidStats(24,
+                states(POWER_STATE_BATTERY, SCREEN_STATE_ON, PROCESS_STATE_FOREGROUND),
+                values(400, 1500, 2000),  1.206947);
+        mStats.setUidStats(42,
+                states(POWER_STATE_BATTERY, SCREEN_STATE_ON, PROCESS_STATE_FOREGROUND),
+                values(900, 1000, 1500), 1.016182);
+        mStats.setUidStats(42,
+                states(POWER_STATE_BATTERY, SCREEN_STATE_ON, PROCESS_STATE_BACKGROUND),
+                values(600, 500, 300), 0.385042);
+        mStats.setUidStats(42,
+                states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED),
+                values(1500, 2000, 1000), 1.252578);
+
+        mProcessor.finish(mStats);
+
+        mStats.verifyPowerEstimates();
+    }
+
+    @Test
+    public void energyConsumerModel() {
+        mStats = new MockPowerComponentAggregatedPowerStats(mConfig, true);
+        mStats.setDeviceStats(
+                states(POWER_STATE_BATTERY, SCREEN_STATE_ON),
+                concat(
+                        values(3500, 4500, 3000),           // scaling steps
+                        values(2000, 1000),                 // clusters
+                        values(5000),                       // uptime
+                        values(5_000_000L, 6_000_000L)),    // energy, uC
+                3.055555);
+        mStats.setDeviceStats(
+                states(POWER_STATE_OTHER, SCREEN_STATE_ON),
+                concat(
+                        values(6000, 6500, 4000),
+                        values(5000, 3000),
+                        values(7000),
+                        values(5_000_000L, 6_000_000L)),    // same as above
+                3.055555);                                  // same as above - WAI
+        mStats.setDeviceStats(
+                states(POWER_STATE_OTHER, SCREEN_STATE_OTHER),
+                concat(
+                        values(9000, 10000, 7000),
+                        values(8000, 6000),
+                        values(20000),
+                        values(8_000_000L, 18_000_000L)),
+                7.222222);
+        mStats.setUidStats(24,
+                states(POWER_STATE_BATTERY, SCREEN_STATE_ON, PROCESS_STATE_FOREGROUND),
+                values(400, 1500, 2000),  1.449078);
+        mStats.setUidStats(42,
+                states(POWER_STATE_BATTERY, SCREEN_STATE_ON, PROCESS_STATE_FOREGROUND),
+                values(900, 1000, 1500), 1.161902);
+        mStats.setUidStats(42,
+                states(POWER_STATE_BATTERY, SCREEN_STATE_ON, PROCESS_STATE_BACKGROUND),
+                values(600, 500, 300), 0.355406);
+        mStats.setUidStats(42,
+                states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED),
+                values(1500, 2000, 1000), 0.80773);
+
+        mProcessor.finish(mStats);
+
+        mStats.verifyPowerEstimates();
+    }
+
+    private int[] states(int... states) {
+        return states;
+    }
+
+    private long[] values(long... values) {
+        return values;
+    }
+
+    private long[] concat(long[]... arrays) {
+        LongArray all = new LongArray();
+        for (long[] array : arrays) {
+            for (long value : array) {
+                all.add(value);
+            }
+        }
+        return all.toArray();
+    }
+
+    private static class MockPowerComponentAggregatedPowerStats extends
+            PowerComponentAggregatedPowerStats {
+        private final CpuPowerStatsCollector.StatsArrayLayout mStatsLayout;
+        private final PowerStats.Descriptor mDescriptor;
+        private HashMap<String, long[]> mDeviceStats = new HashMap<>();
+        private HashMap<String, long[]> mUidStats = new HashMap<>();
+        private HashSet<Integer> mUids = new HashSet<>();
+        private HashMap<String, Double> mExpectedDevicePower = new HashMap<>();
+        private HashMap<String, Double> mExpectedUidPower = new HashMap<>();
+
+        MockPowerComponentAggregatedPowerStats(AggregatedPowerStatsConfig.PowerComponent config,
+                boolean useEnergyConsumers) {
+            super(config);
+            mStatsLayout = new CpuPowerStatsCollector.StatsArrayLayout();
+            mStatsLayout.addDeviceSectionCpuTimeByScalingStep(3);
+            mStatsLayout.addDeviceSectionCpuTimeByCluster(2);
+            mStatsLayout.addDeviceSectionUptime();
+            if (useEnergyConsumers) {
+                mStatsLayout.addDeviceSectionEnergyConsumers(2);
+            }
+            mStatsLayout.addDeviceSectionPowerEstimate();
+            mStatsLayout.addUidSectionCpuTimeByPowerBracket(new int[]{0, 1, 2});
+            mStatsLayout.addUidSectionPowerEstimate();
+
+            PersistableBundle extras = new PersistableBundle();
+            mStatsLayout.toExtras(extras);
+            mDescriptor = new PowerStats.Descriptor(BatteryConsumer.POWER_COMPONENT_CPU,
+                    mStatsLayout.getDeviceStatsArrayLength(), mStatsLayout.getUidStatsArrayLength(),
+                    extras);
+        }
+
+        @Override
+        public PowerStats.Descriptor getPowerStatsDescriptor() {
+            return mDescriptor;
+        }
+
+        @Override
+        boolean getDeviceStats(long[] outValues, int[] deviceStates) {
+            long[] values = getDeviceStats(deviceStates);
+            System.arraycopy(values, 0, outValues, 0, values.length);
+            return true;
+        }
+
+        private long[] getDeviceStats(int[] deviceStates) {
+            String key = statesToString(getConfig().getDeviceStateConfig(), deviceStates);
+            long[] values = mDeviceStats.get(key);
+            return values == null ? new long[mDescriptor.statsArrayLength] : values;
+        }
+
+        void setDeviceStats(int[] states, long[] values, double expectedPowerEstimate) {
+            setDeviceStats(states, values);
+            mExpectedDevicePower.put(statesToString(getConfig().getDeviceStateConfig(), states),
+                    expectedPowerEstimate);
+        }
+
+        @Override
+        void setDeviceStats(int[] states, long[] values) {
+            String key = statesToString(getConfig().getDeviceStateConfig(), states);
+            mDeviceStats.put(key, Arrays.copyOf(values, mDescriptor.statsArrayLength));
+        }
+
+        @Override
+        boolean getUidStats(long[] outValues, int uid, int[] uidStates) {
+            long[] values = getUidStats(uid, uidStates);
+            assertThat(values).isNotNull();
+            System.arraycopy(values, 0, outValues, 0, values.length);
+            return true;
+        }
+
+        private long[] getUidStats(int uid, int[] uidStates) {
+            String key = uid + " " + statesToString(getConfig().getUidStateConfig(), uidStates);
+            long[] values = mUidStats.get(key);
+            return values == null ? new long[mDescriptor.uidStatsArrayLength] : values;
+        }
+
+        void setUidStats(int uid, int[] states, long[] values, double expectedPowerEstimate) {
+            setUidStats(uid, states, values);
+            mExpectedUidPower.put(
+                    uid + " " + statesToString(getConfig().getUidStateConfig(), states),
+                    expectedPowerEstimate);
+        }
+
+        @Override
+        void setUidStats(int uid, int[] states, long[] values) {
+            mUids.add(uid);
+            String key = uid + " " + statesToString(getConfig().getUidStateConfig(), states);
+            mUidStats.put(key, Arrays.copyOf(values, mDescriptor.uidStatsArrayLength));
+        }
+
+        @Override
+        void collectUids(Collection<Integer> uids) {
+            uids.addAll(mUids);
+        }
+
+        void verifyPowerEstimates() {
+            StringBuilder mismatches = new StringBuilder();
+            for (Map.Entry<String, Double> entry : mExpectedDevicePower.entrySet()) {
+                String key = entry.getKey();
+                double expected = mExpectedDevicePower.get(key);
+                double actual = mStatsLayout.getDevicePowerEstimate(mDeviceStats.get(key));
+                if (Math.abs(expected - actual) > 0.005) {
+                    mismatches.append(key + " expected: " + expected + " actual: " + actual + "\n");
+                }
+            }
+            for (Map.Entry<String, Double> entry : mExpectedUidPower.entrySet()) {
+                String key = entry.getKey();
+                double expected = mExpectedUidPower.get(key);
+                double actual = mStatsLayout.getUidPowerEstimate(mUidStats.get(key));
+                if (Math.abs(expected - actual) > 0.005) {
+                    mismatches.append(key + " expected: " + expected + " actual: " + actual + "\n");
+                }
+            }
+            if (!mismatches.isEmpty()) {
+                fail("Unexpected power estimations:\n" + mismatches);
+            }
+        }
+
+        private String statesToString(MultiStateStats.States[] config, int[] states) {
+            StringBuilder sb = new StringBuilder();
+            for (int i = 0; i < states.length; i++) {
+                sb.append(config[i].getName()).append("=").append(states[i]).append(" ");
+            }
+            return sb.toString();
+        }
+    }
+}
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorTest.java
index f2ee6db..bc211df 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorTest.java
@@ -20,16 +20,26 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import android.content.Context;
+import android.hardware.power.stats.EnergyConsumer;
+import android.hardware.power.stats.EnergyConsumerResult;
+import android.hardware.power.stats.EnergyConsumerType;
+import android.os.BatteryConsumer;
 import android.os.ConditionVariable;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.power.PowerStatsInternal;
 import android.util.SparseArray;
 
+import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.frameworks.powerstatstests.R;
 import com.android.internal.os.CpuScalingPolicies;
 import com.android.internal.os.PowerProfile;
 import com.android.internal.os.PowerStats;
@@ -40,85 +50,266 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class CpuPowerStatsCollectorTest {
+    private Context mContext;
     private final MockClock mMockClock = new MockClock();
     private final HandlerThread mHandlerThread = new HandlerThread("test");
     private Handler mHandler;
-    private CpuPowerStatsCollector mCollector;
     private PowerStats mCollectedStats;
-    @Mock
     private PowerProfile mPowerProfile;
     @Mock
     private CpuPowerStatsCollector.KernelCpuStatsReader mMockKernelCpuStatsReader;
+    @Mock
+    private PowerStatsInternal mPowerStatsInternal;
+    private CpuScalingPolicies mCpuScalingPolicies;
 
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
+        mContext = InstrumentationRegistry.getContext();
 
         mHandlerThread.start();
         mHandler = mHandlerThread.getThreadHandler();
-        when(mPowerProfile.getCpuPowerBracketCount()).thenReturn(2);
-        when(mPowerProfile.getCpuPowerBracketForScalingStep(0, 0)).thenReturn(0);
-        when(mPowerProfile.getCpuPowerBracketForScalingStep(0, 1)).thenReturn(1);
-        mCollector = new CpuPowerStatsCollector(new CpuScalingPolicies(
-                new SparseArray<>() {{
-                    put(0, new int[]{0});
-                }},
-                new SparseArray<>() {{
-                    put(0, new int[]{1, 12});
-                }}),
-                mPowerProfile, mHandler, mMockKernelCpuStatsReader, 60_000, mMockClock);
-        mCollector.addConsumer(stats -> mCollectedStats = stats);
-        mCollector.setEnabled(true);
+        when(mMockKernelCpuStatsReader.nativeIsSupportedFeature()).thenReturn(true);
     }
 
     @Test
-    public void collectStats() {
-        mockKernelCpuStats(new SparseArray<>() {{
-                put(42, new long[]{100, 200});
-                put(99, new long[]{300, 600});
-            }}, 0, 1234);
+    public void powerBrackets_specifiedInPowerProfile() {
+        mPowerProfile = new PowerProfile(mContext);
+        mPowerProfile.forceInitForTesting(mContext, R.xml.power_profile_test_power_brackets);
+        mCpuScalingPolicies = new CpuScalingPolicies(
+                new SparseArray<>() {{
+                    put(0, new int[]{0});
+                    put(4, new int[]{4});
+                }},
+                new SparseArray<>() {{
+                    put(0, new int[]{100});
+                    put(4, new int[]{400, 500});
+                }});
+
+        CpuPowerStatsCollector collector = createCollector(8, 0);
+
+        assertThat(getScalingStepToPowerBracketMap(collector))
+                .isEqualTo(new int[]{1, 1, 0});
+    }
+
+    @Test
+    public void powerBrackets_default_noEnergyConsumers() {
+        mPowerProfile = new PowerProfile(mContext);
+        mPowerProfile.forceInitForTesting(mContext, R.xml.power_profile_test);
+        mockCpuScalingPolicies(2);
+
+        CpuPowerStatsCollector collector = createCollector(3, 0);
+
+        assertThat(new String[]{
+                collector.getCpuPowerBracketDescription(0),
+                collector.getCpuPowerBracketDescription(1),
+                collector.getCpuPowerBracketDescription(2)})
+                .isEqualTo(new String[]{
+                        "0/300(10.0)",
+                        "0/1000(20.0), 0/2000(30.0), 4/300(25.0)",
+                        "4/1000(35.0), 4/2500(50.0), 4/3000(60.0)"});
+        assertThat(getScalingStepToPowerBracketMap(collector))
+                .isEqualTo(new int[]{0, 1, 1, 1, 2, 2, 2});
+    }
+
+    @Test
+    public void powerBrackets_moreBracketsThanStates() {
+        mPowerProfile = new PowerProfile(mContext);
+        mPowerProfile.forceInitForTesting(mContext, R.xml.power_profile_test);
+        mockCpuScalingPolicies(2);
+
+        CpuPowerStatsCollector collector = createCollector(8, 0);
+
+        assertThat(getScalingStepToPowerBracketMap(collector))
+                .isEqualTo(new int[]{0, 1, 2, 3, 4, 5, 6});
+    }
+
+    @Test
+    public void powerBrackets_energyConsumers() throws Exception {
+        mPowerProfile = new PowerProfile(mContext);
+        mPowerProfile.forceInitForTesting(mContext, R.xml.power_profile_test);
+        mockCpuScalingPolicies(2);
+        mockEnergyConsumers();
+
+        CpuPowerStatsCollector collector = createCollector(8, 2);
+
+        assertThat(getScalingStepToPowerBracketMap(collector))
+                .isEqualTo(new int[]{0, 1, 1, 2, 2, 3, 3});
+    }
+
+    @Test
+    public void powerStatsDescriptor() throws Exception {
+        mPowerProfile = new PowerProfile(mContext);
+        mPowerProfile.forceInitForTesting(mContext, R.xml.power_profile_test);
+        mockCpuScalingPolicies(2);
+        mockEnergyConsumers();
+
+        CpuPowerStatsCollector collector = createCollector(8, 2);
+        PowerStats.Descriptor descriptor = collector.getPowerStatsDescriptor();
+        assertThat(descriptor.powerComponentId).isEqualTo(BatteryConsumer.POWER_COMPONENT_CPU);
+        assertThat(descriptor.name).isEqualTo("cpu");
+        assertThat(descriptor.statsArrayLength).isEqualTo(13);
+        assertThat(descriptor.uidStatsArrayLength).isEqualTo(5);
+        CpuPowerStatsCollector.StatsArrayLayout layout =
+                new CpuPowerStatsCollector.StatsArrayLayout();
+        layout.fromExtras(descriptor.extras);
+
+        long[] deviceStats = new long[descriptor.statsArrayLength];
+        layout.setTimeByScalingStep(deviceStats, 2, 42);
+        layout.setConsumedEnergy(deviceStats, 1, 43);
+        layout.setUptime(deviceStats, 44);
+        layout.setDevicePowerEstimate(deviceStats, 45);
+
+        long[] uidStats = new long[descriptor.uidStatsArrayLength];
+        layout.setUidTimeByPowerBracket(uidStats, 3, 46);
+        layout.setUidPowerEstimate(uidStats, 47);
+
+        assertThat(layout.getCpuScalingStepCount()).isEqualTo(7);
+        assertThat(layout.getTimeByScalingStep(deviceStats, 2)).isEqualTo(42);
+
+        assertThat(layout.getCpuClusterEnergyConsumerCount()).isEqualTo(2);
+        assertThat(layout.getConsumedEnergy(deviceStats, 1)).isEqualTo(43);
+
+        assertThat(layout.getUptime(deviceStats)).isEqualTo(44);
+
+        assertThat(layout.getDevicePowerEstimate(deviceStats)).isEqualTo(45);
+
+        assertThat(layout.getScalingStepToPowerBracketMap()).isEqualTo(
+                new int[]{0, 1, 1, 2, 2, 3, 3});
+        assertThat(layout.getCpuPowerBracketCount()).isEqualTo(4);
+
+        assertThat(layout.getUidTimeByPowerBracket(uidStats, 3)).isEqualTo(46);
+        assertThat(layout.getUidPowerEstimate(uidStats)).isEqualTo(47);
+    }
+
+    @Test
+    public void collectStats() throws Exception {
+        mockCpuScalingPolicies(1);
+        mockPowerProfile();
+        mockEnergyConsumers();
+
+        CpuPowerStatsCollector collector = createCollector(8, 0);
+        CpuPowerStatsCollector.StatsArrayLayout layout =
+                new CpuPowerStatsCollector.StatsArrayLayout();
+        layout.fromExtras(collector.getPowerStatsDescriptor().extras);
+
+        mockKernelCpuStats(new long[]{1111, 2222, 3333},
+                new SparseArray<>() {{
+                    put(42, new long[]{100, 200});
+                    put(99, new long[]{300, 600});
+                }}, 0, 1234);
 
         mMockClock.uptime = 1000;
-        mCollector.forceSchedule();
+        collector.forceSchedule();
         waitForIdle();
 
         assertThat(mCollectedStats.durationMs).isEqualTo(1234);
-        assertThat(mCollectedStats.uidStats.get(42)).isEqualTo(new long[]{100, 200});
-        assertThat(mCollectedStats.uidStats.get(99)).isEqualTo(new long[]{300, 600});
 
-        mockKernelCpuStats(new SparseArray<>() {{
-                put(42, new long[]{123, 234});
-                put(99, new long[]{345, 678});
-            }}, 1234, 3421);
+        assertThat(layout.getCpuScalingStepCount()).isEqualTo(3);
+        assertThat(layout.getTimeByScalingStep(mCollectedStats.stats, 0)).isEqualTo(1111);
+        assertThat(layout.getTimeByScalingStep(mCollectedStats.stats, 1)).isEqualTo(2222);
+        assertThat(layout.getTimeByScalingStep(mCollectedStats.stats, 2)).isEqualTo(3333);
+
+        assertThat(layout.getConsumedEnergy(mCollectedStats.stats, 0)).isEqualTo(0);
+        assertThat(layout.getConsumedEnergy(mCollectedStats.stats, 1)).isEqualTo(0);
+
+        assertThat(layout.getUidTimeByPowerBracket(mCollectedStats.uidStats.get(42), 0))
+                .isEqualTo(100);
+        assertThat(layout.getUidTimeByPowerBracket(mCollectedStats.uidStats.get(42), 1))
+                .isEqualTo(200);
+        assertThat(layout.getUidTimeByPowerBracket(mCollectedStats.uidStats.get(99), 0))
+                .isEqualTo(300);
+        assertThat(layout.getUidTimeByPowerBracket(mCollectedStats.uidStats.get(99), 1))
+                .isEqualTo(600);
+
+        mockKernelCpuStats(new long[]{5555, 4444, 3333},
+                new SparseArray<>() {{
+                    put(42, new long[]{123, 234});
+                    put(99, new long[]{345, 678});
+                }}, 1234, 3421);
 
         mMockClock.uptime = 2000;
-        mCollector.forceSchedule();
+        collector.forceSchedule();
         waitForIdle();
 
         assertThat(mCollectedStats.durationMs).isEqualTo(3421 - 1234);
-        assertThat(mCollectedStats.uidStats.get(42)).isEqualTo(new long[]{23, 34});
-        assertThat(mCollectedStats.uidStats.get(99)).isEqualTo(new long[]{45, 78});
+
+        assertThat(layout.getTimeByScalingStep(mCollectedStats.stats, 0)).isEqualTo(4444);
+        assertThat(layout.getTimeByScalingStep(mCollectedStats.stats, 1)).isEqualTo(2222);
+        assertThat(layout.getTimeByScalingStep(mCollectedStats.stats, 2)).isEqualTo(0);
+
+        // 500 * 1000 / 3500
+        assertThat(layout.getConsumedEnergy(mCollectedStats.stats, 0)).isEqualTo(143);
+        // 700 * 1000 / 3500
+        assertThat(layout.getConsumedEnergy(mCollectedStats.stats, 1)).isEqualTo(200);
+
+        assertThat(layout.getUidTimeByPowerBracket(mCollectedStats.uidStats.get(42), 0))
+                .isEqualTo(23);
+        assertThat(layout.getUidTimeByPowerBracket(mCollectedStats.uidStats.get(42), 1))
+                .isEqualTo(34);
+        assertThat(layout.getUidTimeByPowerBracket(mCollectedStats.uidStats.get(99), 0))
+                .isEqualTo(45);
+        assertThat(layout.getUidTimeByPowerBracket(mCollectedStats.uidStats.get(99), 1))
+                .isEqualTo(78);
     }
 
-    private void mockKernelCpuStats(SparseArray<long[]> uidToCpuStats,
+    private void mockCpuScalingPolicies(int clusterCount) {
+        SparseArray<int[]> cpus = new SparseArray<>();
+        SparseArray<int[]> freqs = new SparseArray<>();
+        cpus.put(0, new int[]{0, 1, 2, 3});
+        freqs.put(0, new int[]{300000, 1000000, 2000000});
+        if (clusterCount == 2) {
+            cpus.put(4, new int[]{4, 5});
+            freqs.put(4, new int[]{300000, 1000000, 2500000, 3000000});
+        }
+        mCpuScalingPolicies = new CpuScalingPolicies(cpus, freqs);
+    }
+
+    private void mockPowerProfile() {
+        mPowerProfile = mock(PowerProfile.class);
+        when(mPowerProfile.getCpuPowerBracketCount()).thenReturn(2);
+        when(mPowerProfile.getCpuPowerBracketForScalingStep(0, 0)).thenReturn(0);
+        when(mPowerProfile.getCpuPowerBracketForScalingStep(0, 1)).thenReturn(1);
+        when(mPowerProfile.getCpuPowerBracketForScalingStep(0, 2)).thenReturn(1);
+    }
+
+    private CpuPowerStatsCollector createCollector(int defaultCpuPowerBrackets,
+            int defaultCpuPowerBracketsPerEnergyConsumer) {
+        CpuPowerStatsCollector collector = new CpuPowerStatsCollector(mCpuScalingPolicies,
+                mPowerProfile, mHandler, mMockKernelCpuStatsReader, () -> mPowerStatsInternal,
+                () -> 3500, 60_000, mMockClock, defaultCpuPowerBrackets,
+                defaultCpuPowerBracketsPerEnergyConsumer);
+        collector.addConsumer(stats -> mCollectedStats = stats);
+        collector.setEnabled(true);
+        return collector;
+    }
+
+    private void mockKernelCpuStats(long[] deviceStats, SparseArray<long[]> uidToCpuStats,
             long expectedLastUpdateTimestampMs, long newLastUpdateTimestampMs) {
         when(mMockKernelCpuStatsReader.nativeReadCpuStats(
                 any(CpuPowerStatsCollector.KernelCpuStatsCallback.class),
-                any(int[].class), anyLong(), any(long[].class)))
+                any(int[].class), anyLong(), any(long[].class), any(long[].class)))
                 .thenAnswer(invocation -> {
                     CpuPowerStatsCollector.KernelCpuStatsCallback callback =
                             invocation.getArgument(0);
                     int[] powerBucketIndexes = invocation.getArgument(1);
                     long lastTimestamp = invocation.getArgument(2);
-                    long[] tempStats = invocation.getArgument(3);
+                    long[] cpuTimeByScalingStep = invocation.getArgument(3);
+                    long[] tempStats = invocation.getArgument(4);
 
-                    assertThat(powerBucketIndexes).isEqualTo(new int[]{0, 1});
+                    assertThat(powerBucketIndexes).isEqualTo(new int[]{0, 1, 1});
                     assertThat(lastTimestamp / 1000000L).isEqualTo(expectedLastUpdateTimestampMs);
                     assertThat(tempStats).hasLength(2);
 
+                    System.arraycopy(deviceStats, 0, cpuTimeByScalingStep, 0,
+                            cpuTimeByScalingStep.length);
+
                     for (int i = 0; i < uidToCpuStats.size(); i++) {
                         int uid = uidToCpuStats.keyAt(i);
                         long[] cpuStats = uidToCpuStats.valueAt(i);
@@ -129,6 +320,67 @@
                 });
     }
 
+    @SuppressWarnings("unchecked")
+    private void mockEnergyConsumers() throws Exception {
+        when(mPowerStatsInternal.getEnergyConsumerInfo())
+                .thenReturn(new EnergyConsumer[]{
+                        new EnergyConsumer() {{
+                            id = 1;
+                            type = EnergyConsumerType.CPU_CLUSTER;
+                            ordinal = 0;
+                            name = "CPU0";
+                        }},
+                        new EnergyConsumer() {{
+                            id = 2;
+                            type = EnergyConsumerType.CPU_CLUSTER;
+                            ordinal = 1;
+                            name = "CPU4";
+                        }},
+                        new EnergyConsumer() {{
+                            id = 3;
+                            type = EnergyConsumerType.BLUETOOTH;
+                            name = "BT";
+                        }},
+                });
+
+        CompletableFuture<EnergyConsumerResult[]> future1 = mock(CompletableFuture.class);
+        when(future1.get(anyLong(), any(TimeUnit.class)))
+                .thenReturn(new EnergyConsumerResult[]{
+                        new EnergyConsumerResult() {{
+                            id = 1;
+                            energyUWs = 1000;
+                        }},
+                        new EnergyConsumerResult() {{
+                            id = 2;
+                            energyUWs = 2000;
+                        }}
+                });
+
+        CompletableFuture<EnergyConsumerResult[]> future2 = mock(CompletableFuture.class);
+        when(future2.get(anyLong(), any(TimeUnit.class)))
+                .thenReturn(new EnergyConsumerResult[]{
+                        new EnergyConsumerResult() {{
+                            id = 1;
+                            energyUWs = 1500;
+                        }},
+                        new EnergyConsumerResult() {{
+                            id = 2;
+                            energyUWs = 2700;
+                        }}
+                });
+
+        when(mPowerStatsInternal.getEnergyConsumedAsync(eq(new int[]{1, 2})))
+                .thenReturn(future1)
+                .thenReturn(future2);
+    }
+
+    private static int[] getScalingStepToPowerBracketMap(CpuPowerStatsCollector collector) {
+        CpuPowerStatsCollector.StatsArrayLayout layout =
+                new CpuPowerStatsCollector.StatsArrayLayout();
+        layout.fromExtras(collector.getPowerStatsDescriptor().extras);
+        return layout.getScalingStepToPowerBracketMap();
+    }
+
     private void waitForIdle() {
         ConditionVariable done = new ConditionVariable();
         mHandler.post(done::open);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MultiStateStatsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MultiStateStatsTest.java
index 30a73181..eb03a6c 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/MultiStateStatsTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MultiStateStatsTest.java
@@ -180,7 +180,7 @@
 
         StringWriter sw = new StringWriter();
         PrintWriter pw = new PrintWriter(sw, true);
-        multiStateStats.dump(pw);
+        multiStateStats.dump(pw, Arrays::toString);
         assertThat(sw.toString()).isEqualTo(
                 "plugged-in fg [25, 50]\n"
                 + "on-battery fg [25, 50]\n"
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 71007f5..52a5d8f 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java
@@ -156,6 +156,7 @@
         mUserState.setTargetAssignedToAccessibilityButton(COMPONENT_NAME.flattenToString());
         mUserState.setTouchExplorationEnabledLocked(true);
         mUserState.setMagnificationSingleFingerTripleTapEnabledLocked(true);
+        mUserState.setMagnificationTwoFingerTripleTapEnabledLocked(true);
         mUserState.setAutoclickEnabledLocked(true);
         mUserState.setUserNonInteractiveUiTimeoutLocked(30);
         mUserState.setUserInteractiveUiTimeoutLocked(30);
@@ -178,6 +179,7 @@
         assertNull(mUserState.getTargetAssignedToAccessibilityButton());
         assertFalse(mUserState.isTouchExplorationEnabledLocked());
         assertFalse(mUserState.isMagnificationSingleFingerTripleTapEnabledLocked());
+        assertFalse(mUserState.isMagnificationTwoFingerTripleTapEnabledLocked());
         assertFalse(mUserState.isAutoclickEnabledLocked());
         assertEquals(0, mUserState.getUserNonInteractiveUiTimeoutLocked());
         assertEquals(0, mUserState.getUserInteractiveUiTimeoutLocked());
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java
index 3808f30..bfaf4959 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java
@@ -29,6 +29,8 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -38,6 +40,7 @@
 import android.accessibilityservice.IAccessibilityServiceClient;
 import android.accessibilityservice.IAccessibilityServiceConnection;
 import android.accessibilityservice.MagnificationConfig;
+import android.companion.virtual.IVirtualDeviceListener;
 import android.companion.virtual.IVirtualDeviceManager;
 import android.companion.virtual.VirtualDeviceManager;
 import android.content.ComponentName;
@@ -50,6 +53,7 @@
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.util.ArraySet;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
@@ -74,6 +78,7 @@
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
@@ -94,6 +99,9 @@
     @Rule
     public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
 
+    @Rule
+    public SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     @Mock private Context mMockContext;
     @Mock private AccessibilitySecurityPolicy mMockSecurityPolicy;
     @Mock private AccessibilityWindowManager mMockA11yWindowManager;
@@ -114,6 +122,8 @@
 
     @Before
     public void setup() throws RemoteException {
+        mSetFlagsRule.initAllFlagsToReleaseConfigDefault();
+
         MockitoAnnotations.initMocks(this);
         final Resources resources = InstrumentationRegistry.getContext().getResources();
 
@@ -121,6 +131,8 @@
                 resources.getDimensionPixelSize(R.dimen.accessibility_focus_highlight_stroke_width);
         mFocusColorDefaultValue = resources.getColor(R.color.accessibility_focus_highlight_color);
         when(mMockContext.getResources()).thenReturn(resources);
+        when(mMockContext.getMainExecutor())
+                .thenReturn(InstrumentationRegistry.getTargetContext().getMainExecutor());
 
         when(mMockVirtualDeviceManagerInternal.getDeviceIdsForUid(anyInt())).thenReturn(
                 new ArraySet(Set.of(DEVICE_ID)));
@@ -416,6 +428,101 @@
         assertThat(focusStrokeWidth).isEqualTo(mFocusStrokeWidthDefaultValue);
     }
 
+    @Test
+    public void testRegisterProxy_registersVirtualDeviceListener() throws RemoteException {
+        mSetFlagsRule.enableFlags(android.companion.virtual.flags.Flags.FLAG_VDM_PUBLIC_APIS);
+        registerProxy(DISPLAY_ID);
+
+        verify(mMockIVirtualDeviceManager, times(1)).registerVirtualDeviceListener(any());
+    }
+
+    @Test
+    public void testRegisterMultipleProxies_registersOneVirtualDeviceListener()
+            throws RemoteException {
+        mSetFlagsRule.enableFlags(android.companion.virtual.flags.Flags.FLAG_VDM_PUBLIC_APIS);
+        registerProxy(DISPLAY_ID);
+        registerProxy(DISPLAY_2_ID);
+
+        verify(mMockIVirtualDeviceManager, times(1)).registerVirtualDeviceListener(any());
+    }
+
+    @Test
+    public void testUnregisterProxy_unregistersVirtualDeviceListener() throws RemoteException {
+        mSetFlagsRule.enableFlags(android.companion.virtual.flags.Flags.FLAG_VDM_PUBLIC_APIS);
+        registerProxy(DISPLAY_ID);
+
+        mProxyManager.unregisterProxy(DISPLAY_ID);
+
+        verify(mMockIVirtualDeviceManager, times(1)).unregisterVirtualDeviceListener(any());
+    }
+
+    @Test
+    public void testUnregisterProxy_onlyUnregistersVirtualDeviceListenerOnLastProxyRemoval()
+            throws RemoteException {
+        mSetFlagsRule.enableFlags(android.companion.virtual.flags.Flags.FLAG_VDM_PUBLIC_APIS);
+        registerProxy(DISPLAY_ID);
+        registerProxy(DISPLAY_2_ID);
+
+        mProxyManager.unregisterProxy(DISPLAY_ID);
+        verify(mMockIVirtualDeviceManager, never()).unregisterVirtualDeviceListener(any());
+
+        mProxyManager.unregisterProxy(DISPLAY_2_ID);
+        verify(mMockIVirtualDeviceManager, times(1)).unregisterVirtualDeviceListener(any());
+    }
+
+    @Test
+    public void testRegisteredProxy_virtualDeviceClosed_proxyClosed()
+            throws RemoteException {
+        mSetFlagsRule.enableFlags(android.companion.virtual.flags.Flags.FLAG_VDM_PUBLIC_APIS);
+        registerProxy(DISPLAY_ID);
+
+        assertThat(mProxyManager.isProxyedDeviceId(DEVICE_ID)).isTrue();
+        assertThat(mProxyManager.isProxyedDisplay(DISPLAY_ID)).isTrue();
+
+        ArgumentCaptor<IVirtualDeviceListener> listenerArgumentCaptor =
+                ArgumentCaptor.forClass(IVirtualDeviceListener.class);
+        verify(mMockIVirtualDeviceManager, times(1))
+                .registerVirtualDeviceListener(listenerArgumentCaptor.capture());
+
+        listenerArgumentCaptor.getValue().onVirtualDeviceClosed(DEVICE_ID);
+
+        verify(mMockProxySystemSupport, timeout(5_000)).removeDeviceIdLocked(DEVICE_ID);
+
+        assertThat(mProxyManager.isProxyedDeviceId(DEVICE_ID)).isFalse();
+        assertThat(mProxyManager.isProxyedDisplay(DISPLAY_ID)).isFalse();
+    }
+
+    @Test
+    public void testRegisteredProxy_unrelatedVirtualDeviceClosed_proxyNotClosed()
+            throws RemoteException {
+        mSetFlagsRule.enableFlags(android.companion.virtual.flags.Flags.FLAG_VDM_PUBLIC_APIS);
+        registerProxy(DISPLAY_ID);
+
+        assertThat(mProxyManager.isProxyedDeviceId(DEVICE_ID)).isTrue();
+        assertThat(mProxyManager.isProxyedDisplay(DISPLAY_ID)).isTrue();
+
+        ArgumentCaptor<IVirtualDeviceListener> listenerArgumentCaptor =
+                ArgumentCaptor.forClass(IVirtualDeviceListener.class);
+        verify(mMockIVirtualDeviceManager, times(1))
+                .registerVirtualDeviceListener(listenerArgumentCaptor.capture());
+
+        listenerArgumentCaptor.getValue().onVirtualDeviceClosed(DEVICE_ID + 1);
+
+        assertThat(mProxyManager.isProxyedDeviceId(DEVICE_ID)).isTrue();
+        assertThat(mProxyManager.isProxyedDisplay(DISPLAY_ID)).isTrue();
+    }
+
+    @Test
+    public void testRegisterProxy_doesNotRegisterVirtualDeviceListener_flagDisabled()
+            throws RemoteException {
+        mSetFlagsRule.disableFlags(android.companion.virtual.flags.Flags.FLAG_VDM_PUBLIC_APIS);
+        registerProxy(DISPLAY_ID);
+        mProxyManager.unregisterProxy(DISPLAY_ID);
+
+        verify(mMockIVirtualDeviceManager, never()).registerVirtualDeviceListener(any());
+        verify(mMockIVirtualDeviceManager, never()).unregisterVirtualDeviceListener(any());
+    }
+
     private void registerProxy(int displayId) {
         try {
             mProxyManager.registerProxy(mMockAccessibilityServiceClient, displayId, anyInt(),
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index 24a628e..d26d671 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -345,7 +345,7 @@
         assertWithMessage("should not have received intents")
                 .that(getActions(mInjector.mSentIntents)).isEmpty();
         // TODO(b/140868593): should have received a USER_UNLOCK_MSG message as well, but it doesn't
-        // because StorageManager.isUserKeyUnlocked(TEST_PRE_CREATED_USER_ID) returns false - to
+        // because StorageManager.isCeStorageUnlocked(TEST_PRE_CREATED_USER_ID) returns false - to
         // properly fix it, we'd need to move this class to FrameworksMockingServicesTests so we can
         // mock static methods (but moving this class would involve changing the presubmit tests,
         // and the cascade effect goes on...). In fact, a better approach would to not assert the
@@ -648,7 +648,7 @@
         // checking.
         waitForHandlerToComplete(FgThread.getHandler(), HANDLER_WAIT_TIME_MS);
         verify(mInjector.mStorageManagerMock, times(0))
-                .lockUserKey(anyInt());
+                .lockCeStorage(anyInt());
 
         addForegroundUserAndContinueUserSwitch(TEST_USER_ID2, TEST_USER_ID1,
                 numerOfUserSwitches, true);
@@ -663,7 +663,7 @@
         mUserController.finishUserStopped(ussUser1, /* allowDelayedLocking= */ true);
         waitForHandlerToComplete(FgThread.getHandler(), HANDLER_WAIT_TIME_MS);
         verify(mInjector.mStorageManagerMock, times(1))
-                .lockUserKey(TEST_USER_ID);
+                .lockCeStorage(TEST_USER_ID);
     }
 
     /**
@@ -757,7 +757,7 @@
         mUserController.startUser(TEST_USER_ID, USER_START_MODE_BACKGROUND);
 
         verify(mInjector.mStorageManagerMock, never())
-                .unlockUserKey(eq(TEST_USER_ID), anyInt(), any());
+                .unlockCeStorage(eq(TEST_USER_ID), anyInt(), any());
     }
 
     @Test
@@ -1035,7 +1035,7 @@
         mUserController.finishUserStopped(ussUser, delayedLocking);
         waitForHandlerToComplete(FgThread.getHandler(), HANDLER_WAIT_TIME_MS);
         verify(mInjector.mStorageManagerMock, times(expectLocking ? 1 : 0))
-                .lockUserKey(userId);
+                .lockCeStorage(userId);
     }
 
     private void addForegroundUserAndContinueUserSwitch(int newUserId, int expectedOldUserId,
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
index e3e708e..0230d77 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
@@ -47,7 +47,6 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
@@ -65,7 +64,6 @@
 import android.hardware.biometrics.BiometricPrompt;
 import android.hardware.biometrics.IBiometricAuthenticator;
 import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback;
-import android.hardware.biometrics.IBiometricPromptStatusListener;
 import android.hardware.biometrics.IBiometricSensorReceiver;
 import android.hardware.biometrics.IBiometricService;
 import android.hardware.biometrics.IBiometricServiceReceiver;
@@ -1753,45 +1751,6 @@
         verifyNoMoreInteractions(callback);
     }
 
-    @Test
-    public void testRegisterBiometricPromptOnKeyguardCallback_authenticationAlreadyStarted()
-            throws Exception {
-        final IBiometricPromptStatusListener callback =
-                mock(IBiometricPromptStatusListener.class);
-
-        setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG);
-        invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
-                true /* requireConfirmation */, null /* authenticators */);
-        mBiometricService.mImpl.registerBiometricPromptStatusListener(callback);
-
-        verify(callback).onBiometricPromptShowing();
-    }
-
-    @Test
-    public void testRegisterBiometricPromptOnKeyguardCallback_startAuth_dismissDialog()
-            throws Exception {
-        final IBiometricPromptStatusListener listener =
-                mock(IBiometricPromptStatusListener.class);
-        setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
-        mBiometricService.mImpl.registerBiometricPromptStatusListener(listener);
-        waitForIdle();
-
-        verify(listener).onBiometricPromptIdle();
-
-        invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1,
-                true /* requireConfirmation */, null /* authenticators */);
-        waitForIdle();
-
-        verify(listener).onBiometricPromptShowing();
-
-        final byte[] hat = generateRandomHAT();
-        mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed(
-                BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED, hat);
-        waitForIdle();
-
-        verify(listener, times(2)).onBiometricPromptIdle();
-    }
-
     // Helper methods
 
     private int invokeCanAuthenticate(BiometricService service, int authenticators)
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
index fe2ac17..f5d50d1 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
@@ -304,17 +304,17 @@
 
         doAnswer(invocation -> {
             Object[] args = invocation.getArguments();
-            mStorageManager.unlockUserKey(/* userId= */ (int) args[0],
+            mStorageManager.unlockCeStorage(/* userId= */ (int) args[0],
                     /* secret= */ (byte[]) args[2]);
             return null;
-        }).when(sm).unlockUserKey(anyInt(), anyInt(), any());
+        }).when(sm).unlockCeStorage(anyInt(), anyInt(), any());
 
         doAnswer(invocation -> {
             Object[] args = invocation.getArguments();
-            mStorageManager.setUserKeyProtection(/* userId= */ (int) args[0],
+            mStorageManager.setCeStorageProtection(/* userId= */ (int) args[0],
                     /* secret= */ (byte[]) args[1]);
             return null;
-        }).when(sm).setUserKeyProtection(anyInt(), any());
+        }).when(sm).setCeStorageProtection(anyInt(), any());
 
         return sm;
     }
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/FakeStorageManager.java b/services/tests/servicestests/src/com/android/server/locksettings/FakeStorageManager.java
index 91f3fed..c08ad13 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/FakeStorageManager.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/FakeStorageManager.java
@@ -24,7 +24,7 @@
 
     private final ArrayMap<Integer, byte[]> mUserSecrets = new ArrayMap<>();
 
-    public void setUserKeyProtection(int userId, byte[] secret) {
+    public void setCeStorageProtection(int userId, byte[] secret) {
         assertThat(mUserSecrets).doesNotContainKey(userId);
         mUserSecrets.put(userId, secret);
     }
@@ -35,7 +35,7 @@
         return secret;
     }
 
-    public void unlockUserKey(int userId, byte[] secret) {
+    public void unlockCeStorage(int userId, byte[] secret) {
         assertThat(mUserSecrets.get(userId)).isEqualTo(secret);
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
index 4e6dd06..ece3dfe 100644
--- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
@@ -674,6 +674,17 @@
     }
 
     @Test
+    public void notifyPermissionRequestCancelled_forwardsToLogger() {
+        int hostUid = 123;
+        mService =
+                new MediaProjectionManagerService(mContext, mMediaProjectionMetricsLoggerInjector);
+
+        mService.notifyPermissionRequestCancelled(hostUid);
+
+        verify(mMediaProjectionMetricsLogger).logProjectionPermissionRequestCancelled(hostUid);
+    }
+
+    @Test
     public void notifyAppSelectorDisplayed_forwardsToLogger() {
         int hostUid = 456;
         mService =
diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionMetricsLoggerTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionMetricsLoggerTest.java
index 410604f..ad1cd6e 100644
--- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionMetricsLoggerTest.java
+++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionMetricsLoggerTest.java
@@ -18,6 +18,7 @@
 
 import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__CREATION_SOURCE__CREATION_SOURCE_UNKNOWN;
 import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_APP_SELECTOR_DISPLAYED;
+import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_CANCELLED;
 import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_CAPTURING_IN_PROGRESS;
 import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_INITIATED;
 import static com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_PERMISSION_REQUEST_DISPLAYED;
@@ -452,6 +453,63 @@
                 MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_STOPPED);
     }
 
+    @Test
+    public void logProjectionPermissionRequestCancelled_logsStateChangedAtomId() {
+        mLogger.logProjectionPermissionRequestCancelled(TEST_HOST_UID);
+
+        verifyStateChangedAtomIdLogged();
+    }
+
+    @Test
+    public void logProjectionPermissionRequestCancelled_logsExistingSessionId() {
+        int existingSessionId = 456;
+        when(mSessionIdGenerator.getCurrentSessionId()).thenReturn(existingSessionId);
+
+        mLogger.logProjectionPermissionRequestCancelled(TEST_HOST_UID);
+
+        verifySessionIdLogged(existingSessionId);
+    }
+
+    @Test
+    public void logProjectionPermissionRequestCancelled_logsStateCancelled() {
+        mLogger.logProjectionPermissionRequestCancelled(TEST_HOST_UID);
+
+        verifyStateLogged(MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_CANCELLED);
+    }
+
+    @Test
+    public void logProjectionPermissionRequestCancelled_logsPreviousState() {
+        mLogger.logInitiated(TEST_HOST_UID, TEST_CREATION_SOURCE);
+        verifyPreviousStateLogged(
+                MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_UNKNOWN);
+
+        mLogger.logProjectionPermissionRequestCancelled(TEST_HOST_UID);
+        verifyPreviousStateLogged(
+                MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_INITIATED);
+    }
+
+    @Test
+    public void logProjectionPermissionRequestCancelled_logsHostUid() {
+        mLogger.logProjectionPermissionRequestCancelled(TEST_HOST_UID);
+
+        verifyHostUidLogged(TEST_HOST_UID);
+    }
+
+    @Test
+    public void logProjectionPermissionRequestCancelled_logsUnknownTargetUid() {
+        mLogger.logProjectionPermissionRequestCancelled(TEST_HOST_UID);
+
+        verifyTargetUidLogged(-2);
+    }
+
+    @Test
+    public void logProjectionPermissionRequestCancelled_logsUnknownCreationSource() {
+        mLogger.logProjectionPermissionRequestCancelled(TEST_HOST_UID);
+
+        verifyCreationSourceLogged(
+                MEDIA_PROJECTION_STATE_CHANGED__CREATION_SOURCE__CREATION_SOURCE_UNKNOWN);
+    }
+
     private void verifyStateChangedAtomIdLogged() {
         verify(mFrameworkStatsLogWrapper)
                 .write(
diff --git a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java
index fc27edc..a4d50f0 100644
--- a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java
@@ -94,27 +94,31 @@
     public void testBugreportFileManagerFileExists() {
         Pair<Integer, String> callingInfo = new Pair<>(mCallingUid, mCallingPackage);
         mBugreportFileManager.addBugreportFileForCaller(
-                callingInfo, mBugreportFile);
+                callingInfo, mBugreportFile, /* keepOnRetrieval= */ false);
 
         assertThrows(IllegalArgumentException.class, () ->
                 mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(
-                        callingInfo, "unknown-file.zip"));
+                        mContext, callingInfo, Process.myUserHandle().getIdentifier(),
+                        "unknown-file.zip"));
 
         // No exception should be thrown.
-        mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(callingInfo, mBugreportFile);
+        mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(
+                mContext, callingInfo, mContext.getUserId(), mBugreportFile);
     }
 
     @Test
     public void testBugreportFileManagerMultipleFiles() {
         Pair<Integer, String> callingInfo = new Pair<>(mCallingUid, mCallingPackage);
         mBugreportFileManager.addBugreportFileForCaller(
-                callingInfo, mBugreportFile);
+                callingInfo, mBugreportFile, /* keepOnRetrieval= */ false);
         mBugreportFileManager.addBugreportFileForCaller(
-                callingInfo, mBugreportFile2);
+                callingInfo, mBugreportFile2, /* keepOnRetrieval= */ false);
 
         // No exception should be thrown.
-        mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(callingInfo, mBugreportFile);
-        mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(callingInfo, mBugreportFile2);
+        mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(
+                mContext, callingInfo, mContext.getUserId(), mBugreportFile);
+        mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(
+                mContext, callingInfo, mContext.getUserId(), mBugreportFile2);
     }
 
     @Test
@@ -122,7 +126,8 @@
         Pair<Integer, String> callingInfo = new Pair<>(mCallingUid, mCallingPackage);
         assertThrows(IllegalArgumentException.class,
                 () -> mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(
-                        callingInfo, "test-file.zip"));
+                        mContext, callingInfo, Process.myUserHandle().getIdentifier(),
+                        "test-file.zip"));
     }
 
     @Test
@@ -130,7 +135,8 @@
         CountDownLatch latch = new CountDownLatch(1);
         Listener listener = new Listener(latch);
         mService.retrieveBugreport(Binder.getCallingUid(), mContext.getPackageName(),
-                new FileDescriptor(), mBugreportFile, listener);
+                mContext.getUserId(), new FileDescriptor(), mBugreportFile,
+                /* keepOnRetrieval= */ false, listener);
         assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
         assertThat(listener.getErrorCode()).isEqualTo(
                 BugreportCallback.BUGREPORT_ERROR_NO_BUGREPORT_TO_RETRIEVE);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java
index 51b9c17..47f15b8 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java
@@ -18,6 +18,7 @@
 import static com.android.server.notification.SnoozeHelper.CONCURRENT_SNOOZE_LIMIT;
 import static com.android.server.notification.SnoozeHelper.EXTRA_KEY;
 
+import static com.google.common.truth.Truth.assertThat;
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNotNull;
@@ -486,6 +487,29 @@
     }
 
     @Test
+    public void testRepostAll() throws Exception {
+        final int profileId = 11;
+        final int otherUserId = 2;
+        IntArray userIds = new IntArray();
+        userIds.add(UserHandle.USER_SYSTEM);
+        userIds.add(profileId);
+        NotificationRecord r = getNotificationRecord("pkg", 1, "one", UserHandle.SYSTEM);
+        NotificationRecord r2 = getNotificationRecord("pkg", 2, "two", UserHandle.SYSTEM);
+        NotificationRecord r3 = getNotificationRecord("pkg", 3, "three", UserHandle.of(profileId));
+        NotificationRecord r4 = getNotificationRecord("pkg", 4, "four", UserHandle.of(otherUserId));
+        mSnoozeHelper.snooze(r,  1000);
+        mSnoozeHelper.snooze(r2, 1000);
+        mSnoozeHelper.snooze(r3, 1000);
+        mSnoozeHelper.snooze(r4, 1000);
+
+        mSnoozeHelper.repostAll(userIds);
+
+        verify(mCallback, times(3)).repost(anyInt(), any(), anyBoolean());
+        // All notifications were reposted, except the one for otherUserId
+        assertThat(mSnoozeHelper.getSnoozed()).containsExactly(r4);
+    }
+
+    @Test
     public void testGetSnoozedBy() throws Exception {
         NotificationRecord r = getNotificationRecord("pkg", 1, "one", UserHandle.SYSTEM);
         NotificationRecord r2 = getNotificationRecord("pkg", 2, "two", UserHandle.SYSTEM);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index 6235b3b..c57b051 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -1742,6 +1742,7 @@
     private void testApplyTransaction_reorder_failsIfNotSystemOrganizer_common(
             @TaskFragmentOperation.OperationType int opType) {
         final Task task = createTask(mDisplayContent);
+        doNothing().when(task).sendTaskFragmentParentInfoChangedIfNeeded();
         // Create a non-embedded Activity at the bottom.
         final ActivityRecord bottomActivity = new ActivityBuilder(mAtm)
                 .setTask(task)
@@ -1934,7 +1935,7 @@
     /** Setups the mock Task as the parent of the given TaskFragment. */
     private static void setupMockParent(TaskFragment taskFragment, Task mockParent) {
         doReturn(mockParent).when(taskFragment).getTask();
-        doReturn(new TaskFragmentParentInfo(new Configuration(), DEFAULT_DISPLAY, true))
+        doReturn(new TaskFragmentParentInfo(new Configuration(), DEFAULT_DISPLAY, true, true))
                 .when(mockParent).getTaskFragmentParentInfo();
 
         // Task needs to be visible
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 0c58069..435a835 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -1568,6 +1568,7 @@
 
         final TaskFragment fragment = createTaskFragmentWithEmbeddedActivity(task, organizer);
         final ActivityRecord embeddedActivity = fragment.getTopMostActivity();
+        doNothing().when(task).sendTaskFragmentParentInfoChangedIfNeeded();
         task.moveActivityToFront(activity);
         assertEquals("Activity must be moved to front", activity, task.getTopMostActivity());
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index c8546c6..4773023 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -2475,6 +2475,46 @@
         verify(session).close();
     }
 
+    @Test
+    public void testReadyTrackerBasics() {
+        final TransitionController controller = new TestTransitionController(
+                mock(ActivityTaskManagerService.class));
+        controller.setFullReadyTrackingForTest(true);
+        Transition transit = createTestTransition(TRANSIT_OPEN, controller);
+        // Not ready if nothing has happened yet
+        assertFalse(transit.mReadyTracker.isReady());
+
+        Transition.ReadyCondition condition1 = new Transition.ReadyCondition("c1");
+        transit.mReadyTracker.add(condition1);
+        assertFalse(transit.mReadyTracker.isReady());
+
+        Transition.ReadyCondition condition2 = new Transition.ReadyCondition("c2");
+        transit.mReadyTracker.add(condition2);
+        assertFalse(transit.mReadyTracker.isReady());
+
+        condition2.meet();
+        assertFalse(transit.mReadyTracker.isReady());
+
+        condition1.meet();
+        assertTrue(transit.mReadyTracker.isReady());
+    }
+
+    @Test
+    public void testReadyTrackerAlternate() {
+        final TransitionController controller = new TestTransitionController(
+                mock(ActivityTaskManagerService.class));
+        controller.setFullReadyTrackingForTest(true);
+        Transition transit = createTestTransition(TRANSIT_OPEN, controller);
+
+        Transition.ReadyCondition condition1 = new Transition.ReadyCondition("c1");
+        transit.mReadyTracker.add(condition1);
+        assertFalse(transit.mReadyTracker.isReady());
+
+        condition1.meetAlternate("reason1");
+        assertTrue(transit.mReadyTracker.isReady());
+        assertEquals("reason1", condition1.mAlternate);
+    }
+
     private static void makeTaskOrganized(Task... tasks) {
         final ITaskOrganizer organizer = mock(ITaskOrganizer.class);
         for (Task t : tasks) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
index 0b77fd8..cd3fef6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -1646,6 +1646,28 @@
         verify(mWm.mAtmService.mRootWindowContainer).resumeFocusedTasksTopActivities();
     }
 
+    @Test
+    public void testSetAlwaysOnTop() {
+        final Task rootTask = new TaskBuilder(mSupervisor)
+                .setWindowingMode(WINDOWING_MODE_FREEFORM).build();
+        testSetAlwaysOnTop(rootTask);
+
+        final DisplayArea displayArea = mDisplayContent.getDefaultTaskDisplayArea();
+        displayArea.setWindowingMode(WINDOWING_MODE_FREEFORM);
+        testSetAlwaysOnTop(displayArea);
+    }
+
+    private void testSetAlwaysOnTop(WindowContainer wc) {
+        final WindowContainerTransaction t = new WindowContainerTransaction();
+        t.setAlwaysOnTop(wc.mRemoteToken.toWindowContainerToken(), true);
+        mWm.mAtmService.mWindowOrganizerController.applyTransaction(t);
+        assertTrue(wc.isAlwaysOnTop());
+
+        t.setAlwaysOnTop(wc.mRemoteToken.toWindowContainerToken(), false);
+        mWm.mAtmService.mWindowOrganizerController.applyTransaction(t);
+        assertFalse(wc.isAlwaysOnTop());
+    }
+
     private ActivityRecord createActivityRecordAndDispatchPendingEvents(Task task) {
         final ActivityRecord record = createActivityRecord(task);
         // Flush EVENT_APPEARED.
diff --git a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
index 3ec6f42..973ab84 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
@@ -448,7 +448,6 @@
         mDisplayContent.updateImeParent();
 
         // Ime should on top of the popup IME layering target window.
-        mDisplayContent.assignChildLayers(mTransaction);
         assertWindowHigher(mImeWindow, popupImeTargetWin);
     }
 
diff --git a/startop/view_compiler/Android.bp b/startop/view_compiler/Android.bp
deleted file mode 100644
index e172090..0000000
--- a/startop/view_compiler/Android.bp
+++ /dev/null
@@ -1,115 +0,0 @@
-//
-// Copyright (C) 2018 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 {
-    // 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"],
-}
-
-cc_defaults {
-    name: "viewcompiler_defaults",
-    header_libs: [
-        "libbase_headers",
-    ],
-    shared_libs: [
-        "libbase",
-        "slicer",
-    ],
-    static_libs: [
-        "libcutils",
-        "libtinyxml2",
-        "liblog",
-        "libutils",
-        "libziparchive",
-        "libz",
-    ],
-    cpp_std: "gnu++2b",
-    target: {
-        android: {
-            shared_libs: [
-                "libandroidfw",
-            ],
-        },
-        host: {
-            static_libs: [
-                "libandroidfw",
-            ],
-        },
-    },
-}
-
-cc_library_static {
-    name: "libviewcompiler",
-    defaults: ["viewcompiler_defaults"],
-    srcs: [
-        "apk_layout_compiler.cc",
-        "dex_builder.cc",
-        "dex_layout_compiler.cc",
-        "java_lang_builder.cc",
-        "tinyxml_layout_parser.cc",
-        "util.cc",
-        "layout_validation.cc",
-    ],
-    host_supported: true,
-}
-
-cc_binary {
-    name: "viewcompiler",
-    defaults: ["viewcompiler_defaults"],
-    srcs: [
-        "main.cc",
-    ],
-    static_libs: [
-        "libgflags",
-        "libviewcompiler",
-    ],
-    host_supported: true,
-}
-
-cc_test_host {
-    name: "view-compiler-tests",
-    defaults: ["viewcompiler_defaults"],
-    srcs: [
-        "layout_validation_test.cc",
-        "util_test.cc",
-    ],
-    static_libs: [
-        "libviewcompiler",
-    ],
-}
-
-cc_binary_host {
-    name: "dex_testcase_generator",
-    defaults: ["viewcompiler_defaults"],
-    srcs: ["dex_testcase_generator.cc"],
-    static_libs: [
-        "libviewcompiler",
-    ],
-}
-
-genrule {
-    name: "generate_dex_testcases",
-    tools: [":dex_testcase_generator"],
-    cmd: "$(location :dex_testcase_generator) $(genDir)",
-    out: [
-        "simple.dex",
-        "trivial.dex",
-    ],
-}
diff --git a/startop/view_compiler/OWNERS b/startop/view_compiler/OWNERS
deleted file mode 100644
index e5aead9..0000000
--- a/startop/view_compiler/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-eholk@google.com
-mathieuc@google.com
diff --git a/startop/view_compiler/README.md b/startop/view_compiler/README.md
deleted file mode 100644
index f8da02b..0000000
--- a/startop/view_compiler/README.md
+++ /dev/null
@@ -1,53 +0,0 @@
-# View Compiler
-
-This directory contains an experimental compiler for layout files.
-
-It will take a layout XML file and produce a CompiledLayout.java file with a
-specialized layout inflation function.
-
-To use it, let's assume you had a layout in `my_layout.xml` and your app was in
-the Java language package `com.example.myapp`. Run the following command:
-
-    viewcompiler my_layout.xml --package com.example.myapp --out CompiledView.java
-
-This will produce a `CompiledView.java`, which can then be compiled into your
-Android app. Then to use it, in places where you would have inflated
-`R.layouts.my_layout`, instead call `CompiledView.inflate`.
-
-Precompiling views like this generally improves the time needed to inflate them.
-
-This tool is still in its early stages and has a number of limitations.
-* Currently only one layout can be compiled at a time.
-* `merge` and `include` nodes are not supported.
-* View compilation is a manual process that requires code changes in the
-  application.
-* This only works for apps that do not use a custom layout inflater.
-* Other limitations yet to be discovered.
-
-## DexBuilder Tests
-
-The DexBuilder has several low-level end to end tests to verify generated DEX
-code validates, runs, and has the correct behavior. There are, unfortunately, a
-number of pieces that must be added to generate new tests. Here are the
-components:
-
-* `dex_testcase_generator` - Written in C++ using `DexBuilder`. This runs as a
-  build step produce the DEX files that will be tested on device. See the
-  `genrule` named `generate_dex_testcases` in `Android.bp`. These files are then
-  copied over to the device by TradeFed when running tests.
-* `DexBuilderTest` - This is a Java Language test harness that loads the
-  generated DEX files and exercises methods in the file.
-
-To add a new DEX file test, follow these steps:
-1. Modify `dex_testcase_generator` to produce the DEX file.
-2. Add the filename to the `out` list of the `generate_dex_testcases` rule in
-   `Android.bp`.
-3. Add a new `push` option to `AndroidTest.xml` to copy the DEX file to the
-   device.
-4. Modify `DexBuilderTest.java` to load and exercise the new test.
-
-In each case, you should be able to cargo-cult the existing test cases.
-
-In general, you can probably get by without adding a new generated DEX file, and
-instead add more methods to the files that are already generated. In this case,
-you can skip all of steps 2 and 3 above, and simplify steps 1 and 4.
diff --git a/startop/view_compiler/apk_layout_compiler.cc b/startop/view_compiler/apk_layout_compiler.cc
deleted file mode 100644
index 5f5652c..0000000
--- a/startop/view_compiler/apk_layout_compiler.cc
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * Copyright (C) 2018 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.
- */
-
-#include "apk_layout_compiler.h"
-#include "dex_layout_compiler.h"
-#include "java_lang_builder.h"
-#include "layout_validation.h"
-#include "util.h"
-
-#include "androidfw/ApkAssets.h"
-#include "androidfw/AssetManager2.h"
-#include "androidfw/ResourceTypes.h"
-
-#include <iostream>
-#include <locale>
-
-#include "android-base/stringprintf.h"
-
-namespace startop {
-
-using android::ResXMLParser;
-using android::base::StringPrintf;
-
-class ResXmlVisitorAdapter {
- public:
-  ResXmlVisitorAdapter(ResXMLParser* parser) : parser_{parser} {}
-
-  template <typename Visitor>
-  void Accept(Visitor* visitor) {
-    size_t depth{0};
-    do {
-      switch (parser_->next()) {
-        case ResXMLParser::START_DOCUMENT:
-          depth++;
-          visitor->VisitStartDocument();
-          break;
-        case ResXMLParser::END_DOCUMENT:
-          depth--;
-          visitor->VisitEndDocument();
-          break;
-        case ResXMLParser::START_TAG: {
-          depth++;
-          size_t name_length = 0;
-          const char16_t* name = parser_->getElementName(&name_length);
-          visitor->VisitStartTag(std::u16string{name, name_length});
-          break;
-        }
-        case ResXMLParser::END_TAG:
-          depth--;
-          visitor->VisitEndTag();
-          break;
-        default:;
-      }
-    } while (depth > 0 || parser_->getEventType() == ResXMLParser::FIRST_CHUNK_CODE);
-  }
-
- private:
-  ResXMLParser* parser_;
-};
-
-bool CanCompileLayout(ResXMLParser* parser) {
-  ResXmlVisitorAdapter adapter{parser};
-  LayoutValidationVisitor visitor;
-  adapter.Accept(&visitor);
-
-  return visitor.can_compile();
-}
-
-namespace {
-void CompileApkAssetsLayouts(const android::ApkAssetsPtr& assets, CompilationTarget target,
-                             std::ostream& target_out) {
-  android::AssetManager2 resources;
-  resources.SetApkAssets({assets});
-
-  std::string package_name;
-
-  // TODO: handle multiple packages better
-  bool first = true;
-  for (const auto& package : assets->GetLoadedArsc()->GetPackages()) {
-    CHECK(first);
-    package_name = package->GetPackageName();
-    first = false;
-  }
-
-  dex::DexBuilder dex_file;
-  dex::ClassBuilder compiled_view{
-      dex_file.MakeClass(StringPrintf("%s.CompiledView", package_name.c_str()))};
-  std::vector<dex::MethodBuilder> methods;
-
-  assets->GetAssetsProvider()->ForEachFile("res/", [&](android::StringPiece s, android::FileType) {
-      if (s == "layout") {
-          auto path = StringPrintf("res/%.*s/", (int)s.size(), s.data());
-          assets->GetAssetsProvider()
-                  ->ForEachFile(path, [&](android::StringPiece layout_file, android::FileType) {
-                      auto layout_path = StringPrintf("%s%.*s", path.c_str(),
-                                                      (int)layout_file.size(), layout_file.data());
-                      android::ApkAssetsCookie cookie = android::kInvalidCookie;
-                      auto asset = resources.OpenNonAsset(layout_path,
-                                                          android::Asset::ACCESS_RANDOM, &cookie);
-                      CHECK(asset);
-                      CHECK(android::kInvalidCookie != cookie);
-                      const auto dynamic_ref_table = resources.GetDynamicRefTableForCookie(cookie);
-                      CHECK(nullptr != dynamic_ref_table);
-                      android::ResXMLTree xml_tree{dynamic_ref_table};
-                      xml_tree.setTo(asset->getBuffer(/*wordAligned=*/true), asset->getLength(),
-                                     /*copy_data=*/true);
-                      android::ResXMLParser parser{xml_tree};
-                      parser.restart();
-                      if (CanCompileLayout(&parser)) {
-                          parser.restart();
-                          const std::string layout_name =
-                                  startop::util::FindLayoutNameFromFilename(layout_path);
-                          ResXmlVisitorAdapter adapter{&parser};
-                          switch (target) {
-                              case CompilationTarget::kDex: {
-                                  methods.push_back(compiled_view.CreateMethod(
-                                          layout_name,
-                                          dex::Prototype{dex::TypeDescriptor::FromClassname(
-                                                                 "android.view.View"),
-                                                         dex::TypeDescriptor::FromClassname(
-                                                                 "android.content.Context"),
-                                                         dex::TypeDescriptor::Int()}));
-                                  DexViewBuilder builder(&methods.back());
-                                  builder.Start();
-                                  LayoutCompilerVisitor visitor{&builder};
-                                  adapter.Accept(&visitor);
-                                  builder.Finish();
-                                  methods.back().Encode();
-                                  break;
-                              }
-                              case CompilationTarget::kJavaLanguage: {
-                                  JavaLangViewBuilder builder{package_name, layout_name,
-                                                              target_out};
-                                  builder.Start();
-                                  LayoutCompilerVisitor visitor{&builder};
-                                  adapter.Accept(&visitor);
-                                  builder.Finish();
-                                  break;
-                              }
-                          }
-                      }
-                  });
-      }
-  });
-
-  if (target == CompilationTarget::kDex) {
-    slicer::MemView image{dex_file.CreateImage()};
-    target_out.write(image.ptr<const char>(), image.size());
-  }
-}
-}  // namespace
-
-void CompileApkLayouts(const std::string& filename, CompilationTarget target,
-                       std::ostream& target_out) {
-  auto assets = android::ApkAssets::Load(filename);
-  CompileApkAssetsLayouts(assets, target, target_out);
-}
-
-void CompileApkLayoutsFd(android::base::unique_fd fd, CompilationTarget target,
-                         std::ostream& target_out) {
-  constexpr const char* friendly_name{"viewcompiler assets"};
-  auto assets = android::ApkAssets::LoadFromFd(std::move(fd), friendly_name);
-  CompileApkAssetsLayouts(assets, target, target_out);
-}
-
-}  // namespace startop
diff --git a/startop/view_compiler/apk_layout_compiler.h b/startop/view_compiler/apk_layout_compiler.h
deleted file mode 100644
index 03bd545..0000000
--- a/startop/view_compiler/apk_layout_compiler.h
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2018 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.
- */
-
-#ifndef APK_LAYOUT_COMPILER_H_
-#define APK_LAYOUT_COMPILER_H_
-
-#include <string>
-
-#include "android-base/unique_fd.h"
-
-namespace startop {
-
-enum class CompilationTarget { kJavaLanguage, kDex };
-
-void CompileApkLayouts(const std::string& filename, CompilationTarget target,
-                       std::ostream& target_out);
-void CompileApkLayoutsFd(android::base::unique_fd fd, CompilationTarget target,
-                         std::ostream& target_out);
-
-}  // namespace startop
-
-#endif  // APK_LAYOUT_COMPILER_H_
\ No newline at end of file
diff --git a/startop/view_compiler/dex_builder.cc b/startop/view_compiler/dex_builder.cc
deleted file mode 100644
index 50cf5a50..0000000
--- a/startop/view_compiler/dex_builder.cc
+++ /dev/null
@@ -1,704 +0,0 @@
-/*
- * Copyright (C) 2018 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.
- */
-
-#include "dex_builder.h"
-
-#include <fstream>
-#include <memory>
-
-namespace startop {
-namespace dex {
-
-using std::shared_ptr;
-using std::string;
-
-using ::dex::kAccPublic;
-using Op = Instruction::Op;
-
-const TypeDescriptor TypeDescriptor::Int() { return TypeDescriptor{"I"}; };
-const TypeDescriptor TypeDescriptor::Void() { return TypeDescriptor{"V"}; };
-
-namespace {
-// From https://source.android.com/devices/tech/dalvik/dex-format#dex-file-magic
-constexpr uint8_t kDexFileMagic[]{0x64, 0x65, 0x78, 0x0a, 0x30, 0x33, 0x38, 0x00};
-
-// Strings lengths can be 32 bits long, but encoded as LEB128 this can take up to five bytes.
-constexpr size_t kMaxEncodedStringLength{5};
-
-// Converts invoke-* to invoke-*/range
-constexpr ::dex::Opcode InvokeToInvokeRange(::dex::Opcode opcode) {
-  switch (opcode) {
-    case ::dex::Opcode::OP_INVOKE_VIRTUAL:
-      return ::dex::Opcode::OP_INVOKE_VIRTUAL_RANGE;
-    case ::dex::Opcode::OP_INVOKE_DIRECT:
-      return ::dex::Opcode::OP_INVOKE_DIRECT_RANGE;
-    case ::dex::Opcode::OP_INVOKE_STATIC:
-      return ::dex::Opcode::OP_INVOKE_STATIC_RANGE;
-    case ::dex::Opcode::OP_INVOKE_INTERFACE:
-      return ::dex::Opcode::OP_INVOKE_INTERFACE_RANGE;
-    default:
-      LOG(FATAL) << opcode << " is not a recognized invoke opcode.";
-      __builtin_unreachable();
-  }
-}
-
-std::string DotToDescriptor(const char* class_name) {
-  std::string descriptor(class_name);
-  std::replace(descriptor.begin(), descriptor.end(), '.', '/');
-  if (descriptor.length() > 0 && descriptor[0] != '[') {
-    descriptor = "L" + descriptor + ";";
-  }
-  return descriptor;
-}
-
-}  // namespace
-
-std::ostream& operator<<(std::ostream& out, const Instruction::Op& opcode) {
-  switch (opcode) {
-    case Instruction::Op::kReturn:
-      out << "kReturn";
-      return out;
-    case Instruction::Op::kReturnObject:
-      out << "kReturnObject";
-      return out;
-    case Instruction::Op::kMove:
-      out << "kMove";
-      return out;
-    case Instruction::Op::kMoveObject:
-      out << "kMoveObject";
-      return out;
-    case Instruction::Op::kInvokeVirtual:
-      out << "kInvokeVirtual";
-      return out;
-    case Instruction::Op::kInvokeDirect:
-      out << "kInvokeDirect";
-      return out;
-    case Instruction::Op::kInvokeStatic:
-      out << "kInvokeStatic";
-      return out;
-    case Instruction::Op::kInvokeInterface:
-      out << "kInvokeInterface";
-      return out;
-    case Instruction::Op::kBindLabel:
-      out << "kBindLabel";
-      return out;
-    case Instruction::Op::kBranchEqz:
-      out << "kBranchEqz";
-      return out;
-    case Instruction::Op::kBranchNEqz:
-      out << "kBranchNEqz";
-      return out;
-    case Instruction::Op::kNew:
-      out << "kNew";
-      return out;
-    case Instruction::Op::kCheckCast:
-      out << "kCheckCast";
-      return out;
-    case Instruction::Op::kGetStaticField:
-      out << "kGetStaticField";
-      return out;
-    case Instruction::Op::kSetStaticField:
-      out << "kSetStaticField";
-      return out;
-    case Instruction::Op::kGetInstanceField:
-      out << "kGetInstanceField";
-      return out;
-    case Instruction::Op::kSetInstanceField:
-      out << "kSetInstanceField";
-      return out;
-  }
-}
-
-std::ostream& operator<<(std::ostream& out, const Value& value) {
-  if (value.is_register()) {
-    out << "Register(" << value.value() << ")";
-  } else if (value.is_parameter()) {
-    out << "Parameter(" << value.value() << ")";
-  } else if (value.is_immediate()) {
-    out << "Immediate(" << value.value() << ")";
-  } else if (value.is_string()) {
-    out << "String(" << value.value() << ")";
-  } else if (value.is_label()) {
-    out << "Label(" << value.value() << ")";
-  } else if (value.is_type()) {
-    out << "Type(" << value.value() << ")";
-  } else {
-    out << "UnknownValue";
-  }
-  return out;
-}
-
-void* TrackingAllocator::Allocate(size_t size) {
-  std::unique_ptr<uint8_t[]> buffer = std::make_unique<uint8_t[]>(size);
-  void* raw_buffer = buffer.get();
-  allocations_[raw_buffer] = std::move(buffer);
-  return raw_buffer;
-}
-
-void TrackingAllocator::Free(void* ptr) { allocations_.erase(allocations_.find(ptr)); }
-
-// Write out a DEX file that is basically:
-//
-// package dextest;
-// public class DexTest {
-//     public static int foo(String s) { return s.length(); }
-// }
-void WriteTestDexFile(const string& filename) {
-  DexBuilder dex_file;
-
-  ClassBuilder cbuilder{dex_file.MakeClass("dextest.DexTest")};
-  cbuilder.set_source_file("dextest.java");
-
-  TypeDescriptor string_type = TypeDescriptor::FromClassname("java.lang.String");
-
-  MethodBuilder method{cbuilder.CreateMethod("foo", Prototype{TypeDescriptor::Int(), string_type})};
-
-  LiveRegister result = method.AllocRegister();
-
-  MethodDeclData string_length =
-      dex_file.GetOrDeclareMethod(string_type, "length", Prototype{TypeDescriptor::Int()});
-
-  method.AddInstruction(Instruction::InvokeVirtual(string_length.id, result, Value::Parameter(0)));
-  method.BuildReturn(result);
-
-  method.Encode();
-
-  slicer::MemView image{dex_file.CreateImage()};
-
-  std::ofstream out_file(filename);
-  out_file.write(image.ptr<const char>(), image.size());
-}
-
-TypeDescriptor TypeDescriptor::FromClassname(const std::string& name) {
-  return TypeDescriptor{DotToDescriptor(name.c_str())};
-}
-
-DexBuilder::DexBuilder() : dex_file_{std::make_shared<ir::DexFile>()} {
-  dex_file_->magic = slicer::MemView{kDexFileMagic, sizeof(kDexFileMagic)};
-}
-
-slicer::MemView DexBuilder::CreateImage() {
-  ::dex::Writer writer(dex_file_);
-  size_t image_size{0};
-  ::dex::u1* image = writer.CreateImage(&allocator_, &image_size);
-  return slicer::MemView{image, image_size};
-}
-
-ir::String* DexBuilder::GetOrAddString(const std::string& string) {
-  ir::String*& entry = strings_[string];
-
-  if (entry == nullptr) {
-    // Need to encode the length and then write out the bytes, including 1 byte for null terminator
-    auto buffer = std::make_unique<uint8_t[]>(string.size() + kMaxEncodedStringLength + 1);
-    uint8_t* string_data_start = ::dex::WriteULeb128(buffer.get(), string.size());
-
-    size_t header_length =
-        reinterpret_cast<uintptr_t>(string_data_start) - reinterpret_cast<uintptr_t>(buffer.get());
-
-    auto end = std::copy(string.begin(), string.end(), string_data_start);
-    *end = '\0';
-
-    entry = Alloc<ir::String>();
-    // +1 for null terminator
-    entry->data = slicer::MemView{buffer.get(), header_length + string.size() + 1};
-    ::dex::u4 const new_index = dex_file_->strings_indexes.AllocateIndex();
-    dex_file_->strings_map[new_index] = entry;
-    entry->orig_index = new_index;
-    string_data_.push_back(std::move(buffer));
-  }
-  return entry;
-}
-
-ClassBuilder DexBuilder::MakeClass(const std::string& name) {
-  auto* class_def = Alloc<ir::Class>();
-  ir::Type* type_def = GetOrAddType(DotToDescriptor(name.c_str()));
-  type_def->class_def = class_def;
-
-  class_def->type = type_def;
-  class_def->super_class = GetOrAddType(DotToDescriptor("java.lang.Object"));
-  class_def->access_flags = kAccPublic;
-  return ClassBuilder{this, name, class_def};
-}
-
-ir::Type* DexBuilder::GetOrAddType(const std::string& descriptor) {
-  if (types_by_descriptor_.find(descriptor) != types_by_descriptor_.end()) {
-    return types_by_descriptor_[descriptor];
-  }
-
-  ir::Type* type = Alloc<ir::Type>();
-  type->descriptor = GetOrAddString(descriptor);
-  types_by_descriptor_[descriptor] = type;
-  type->orig_index = dex_file_->types_indexes.AllocateIndex();
-  dex_file_->types_map[type->orig_index] = type;
-  return type;
-}
-
-ir::FieldDecl* DexBuilder::GetOrAddField(TypeDescriptor parent, const std::string& name,
-                                         TypeDescriptor type) {
-  const auto key = std::make_tuple(parent, name);
-  if (field_decls_by_key_.find(key) != field_decls_by_key_.end()) {
-    return field_decls_by_key_[key];
-  }
-
-  ir::FieldDecl* field = Alloc<ir::FieldDecl>();
-  field->parent = GetOrAddType(parent);
-  field->name = GetOrAddString(name);
-  field->type = GetOrAddType(type);
-  field->orig_index = dex_file_->fields_indexes.AllocateIndex();
-  dex_file_->fields_map[field->orig_index] = field;
-  field_decls_by_key_[key] = field;
-  return field;
-}
-
-ir::Proto* Prototype::Encode(DexBuilder* dex) const {
-  auto* proto = dex->Alloc<ir::Proto>();
-  proto->shorty = dex->GetOrAddString(Shorty());
-  proto->return_type = dex->GetOrAddType(return_type_.descriptor());
-  if (param_types_.size() > 0) {
-    proto->param_types = dex->Alloc<ir::TypeList>();
-    for (const auto& param_type : param_types_) {
-      proto->param_types->types.push_back(dex->GetOrAddType(param_type.descriptor()));
-    }
-  } else {
-    proto->param_types = nullptr;
-  }
-  return proto;
-}
-
-std::string Prototype::Shorty() const {
-  std::string shorty;
-  shorty.append(return_type_.short_descriptor());
-  for (const auto& type_descriptor : param_types_) {
-    shorty.append(type_descriptor.short_descriptor());
-  }
-  return shorty;
-}
-
-const TypeDescriptor& Prototype::ArgType(size_t index) const {
-  CHECK_LT(index, param_types_.size());
-  return param_types_[index];
-}
-
-ClassBuilder::ClassBuilder(DexBuilder* parent, const std::string& name, ir::Class* class_def)
-    : parent_(parent), type_descriptor_{TypeDescriptor::FromClassname(name)}, class_(class_def) {}
-
-MethodBuilder ClassBuilder::CreateMethod(const std::string& name, Prototype prototype) {
-  ir::MethodDecl* decl = parent_->GetOrDeclareMethod(type_descriptor_, name, prototype).decl;
-
-  return MethodBuilder{parent_, class_, decl};
-}
-
-void ClassBuilder::set_source_file(const string& source) {
-  class_->source_file = parent_->GetOrAddString(source);
-}
-
-MethodBuilder::MethodBuilder(DexBuilder* dex, ir::Class* class_def, ir::MethodDecl* decl)
-    : dex_{dex}, class_{class_def}, decl_{decl} {}
-
-ir::EncodedMethod* MethodBuilder::Encode() {
-  auto* method = dex_->Alloc<ir::EncodedMethod>();
-  method->decl = decl_;
-
-  // TODO: make access flags configurable
-  method->access_flags = kAccPublic | ::dex::kAccStatic;
-
-  auto* code = dex_->Alloc<ir::Code>();
-  CHECK(decl_->prototype != nullptr);
-  size_t const num_args =
-      decl_->prototype->param_types != nullptr ? decl_->prototype->param_types->types.size() : 0;
-  code->registers = NumRegisters() + num_args + kMaxScratchRegisters;
-  code->ins_count = num_args;
-  EncodeInstructions();
-  code->instructions = slicer::ArrayView<const ::dex::u2>(buffer_.data(), buffer_.size());
-  size_t const return_count = decl_->prototype->return_type == dex_->GetOrAddType("V") ? 0 : 1;
-  code->outs_count = std::max(return_count, max_args_);
-  method->code = code;
-
-  class_->direct_methods.push_back(method);
-
-  return method;
-}
-
-LiveRegister MethodBuilder::AllocRegister() {
-  // Find a free register
-  for (size_t i = 0; i < register_liveness_.size(); ++i) {
-    if (!register_liveness_[i]) {
-      register_liveness_[i] = true;
-      return LiveRegister{&register_liveness_, i};
-    }
-  }
-
-  // If we get here, all the registers are in use, so we have to allocate a new
-  // one.
-  register_liveness_.push_back(true);
-  return LiveRegister{&register_liveness_, register_liveness_.size() - 1};
-}
-
-Value MethodBuilder::MakeLabel() {
-  labels_.push_back({});
-  return Value::Label(labels_.size() - 1);
-}
-
-void MethodBuilder::AddInstruction(Instruction instruction) {
-  instructions_.push_back(instruction);
-}
-
-void MethodBuilder::BuildReturn() { AddInstruction(Instruction::OpNoArgs(Op::kReturn)); }
-
-void MethodBuilder::BuildReturn(Value src, bool is_object) {
-  AddInstruction(Instruction::OpWithArgs(
-      is_object ? Op::kReturnObject : Op::kReturn, /*destination=*/{}, src));
-}
-
-void MethodBuilder::BuildConst4(Value target, int value) {
-  CHECK_LT(value, 16);
-  AddInstruction(Instruction::OpWithArgs(Op::kMove, target, Value::Immediate(value)));
-}
-
-void MethodBuilder::BuildConstString(Value target, const std::string& value) {
-  const ir::String* const dex_string = dex_->GetOrAddString(value);
-  AddInstruction(Instruction::OpWithArgs(Op::kMove, target, Value::String(dex_string->orig_index)));
-}
-
-void MethodBuilder::EncodeInstructions() {
-  buffer_.clear();
-  for (const auto& instruction : instructions_) {
-    EncodeInstruction(instruction);
-  }
-}
-
-void MethodBuilder::EncodeInstruction(const Instruction& instruction) {
-  switch (instruction.opcode()) {
-    case Instruction::Op::kReturn:
-      return EncodeReturn(instruction, ::dex::Opcode::OP_RETURN);
-    case Instruction::Op::kReturnObject:
-      return EncodeReturn(instruction, ::dex::Opcode::OP_RETURN_OBJECT);
-    case Instruction::Op::kMove:
-    case Instruction::Op::kMoveObject:
-      return EncodeMove(instruction);
-    case Instruction::Op::kInvokeVirtual:
-      return EncodeInvoke(instruction, ::dex::Opcode::OP_INVOKE_VIRTUAL);
-    case Instruction::Op::kInvokeDirect:
-      return EncodeInvoke(instruction, ::dex::Opcode::OP_INVOKE_DIRECT);
-    case Instruction::Op::kInvokeStatic:
-      return EncodeInvoke(instruction, ::dex::Opcode::OP_INVOKE_STATIC);
-    case Instruction::Op::kInvokeInterface:
-      return EncodeInvoke(instruction, ::dex::Opcode::OP_INVOKE_INTERFACE);
-    case Instruction::Op::kBindLabel:
-      return BindLabel(instruction.args()[0]);
-    case Instruction::Op::kBranchEqz:
-      return EncodeBranch(::dex::Opcode::OP_IF_EQZ, instruction);
-    case Instruction::Op::kBranchNEqz:
-      return EncodeBranch(::dex::Opcode::OP_IF_NEZ, instruction);
-    case Instruction::Op::kNew:
-      return EncodeNew(instruction);
-    case Instruction::Op::kCheckCast:
-      return EncodeCast(instruction);
-    case Instruction::Op::kGetStaticField:
-    case Instruction::Op::kSetStaticField:
-    case Instruction::Op::kGetInstanceField:
-    case Instruction::Op::kSetInstanceField:
-      return EncodeFieldOp(instruction);
-  }
-}
-
-void MethodBuilder::EncodeReturn(const Instruction& instruction, ::dex::Opcode opcode) {
-  CHECK(!instruction.dest().has_value());
-  if (instruction.args().size() == 0) {
-    Encode10x(::dex::Opcode::OP_RETURN_VOID);
-  } else {
-    CHECK_EQ(1, instruction.args().size());
-    size_t source = RegisterValue(instruction.args()[0]);
-    Encode11x(opcode, source);
-  }
-}
-
-void MethodBuilder::EncodeMove(const Instruction& instruction) {
-  CHECK(Instruction::Op::kMove == instruction.opcode() ||
-        Instruction::Op::kMoveObject == instruction.opcode());
-  CHECK(instruction.dest().has_value());
-  CHECK(instruction.dest()->is_variable());
-  CHECK_EQ(1, instruction.args().size());
-
-  const Value& source = instruction.args()[0];
-
-  if (source.is_immediate()) {
-    // TODO: support more registers
-    CHECK_LT(RegisterValue(*instruction.dest()), 16);
-    Encode11n(::dex::Opcode::OP_CONST_4, RegisterValue(*instruction.dest()), source.value());
-  } else if (source.is_string()) {
-    constexpr size_t kMaxRegisters = 256;
-    CHECK_LT(RegisterValue(*instruction.dest()), kMaxRegisters);
-    CHECK_LT(source.value(), 65536);  // make sure we don't need a jumbo string
-    Encode21c(::dex::Opcode::OP_CONST_STRING, RegisterValue(*instruction.dest()), source.value());
-  } else if (source.is_variable()) {
-    // For the moment, we only use this when we need to reshuffle registers for
-    // an invoke instruction, meaning we are too big for the 4-bit version.
-    // We'll err on the side of caution and always generate the 16-bit form of
-    // the instruction.
-    auto opcode = instruction.opcode() == Instruction::Op::kMove
-                        ? ::dex::Opcode::OP_MOVE_16
-                        : ::dex::Opcode::OP_MOVE_OBJECT_16;
-    Encode32x(opcode, RegisterValue(*instruction.dest()), RegisterValue(source));
-  } else {
-    UNIMPLEMENTED(FATAL);
-  }
-}
-
-void MethodBuilder::EncodeInvoke(const Instruction& instruction, ::dex::Opcode opcode) {
-  constexpr size_t kMaxArgs = 5;
-
-  // Currently, we only support up to 5 arguments.
-  CHECK_LE(instruction.args().size(), kMaxArgs);
-
-  uint8_t arguments[kMaxArgs]{};
-  bool has_long_args = false;
-  for (size_t i = 0; i < instruction.args().size(); ++i) {
-    CHECK(instruction.args()[i].is_variable());
-    arguments[i] = RegisterValue(instruction.args()[i]);
-    if (!IsShortRegister(arguments[i])) {
-      has_long_args = true;
-    }
-  }
-
-  if (has_long_args) {
-    // Some of the registers don't fit in the four bit short form of the invoke
-    // instruction, so we need to do an invoke/range. To do this, we need to
-    // first move all the arguments into contiguous temporary registers.
-    std::array<Value, kMaxArgs> scratch = GetScratchRegisters<kMaxArgs>();
-
-    const auto& prototype = dex_->GetPrototypeByMethodId(instruction.index_argument());
-    CHECK(prototype.has_value());
-
-    for (size_t i = 0; i < instruction.args().size(); ++i) {
-      Instruction::Op move_op;
-      if (opcode == ::dex::Opcode::OP_INVOKE_VIRTUAL ||
-          opcode == ::dex::Opcode::OP_INVOKE_DIRECT) {
-        // In this case, there is an implicit `this` argument, which is always an object.
-        if (i == 0) {
-          move_op = Instruction::Op::kMoveObject;
-        } else {
-          move_op = prototype->ArgType(i - 1).is_object() ? Instruction::Op::kMoveObject
-                                                          : Instruction::Op::kMove;
-        }
-      } else {
-        move_op = prototype->ArgType(i).is_object() ? Instruction::Op::kMoveObject
-                                                    : Instruction::Op::kMove;
-      }
-
-      EncodeMove(Instruction::OpWithArgs(move_op, scratch[i], instruction.args()[i]));
-    }
-
-    Encode3rc(InvokeToInvokeRange(opcode),
-              instruction.args().size(),
-              instruction.index_argument(),
-              RegisterValue(scratch[0]));
-  } else {
-    Encode35c(opcode,
-              instruction.args().size(),
-              instruction.index_argument(),
-              arguments[0],
-              arguments[1],
-              arguments[2],
-              arguments[3],
-              arguments[4]);
-  }
-
-  // If there is a return value, add a move-result instruction
-  if (instruction.dest().has_value()) {
-    Encode11x(instruction.result_is_object() ? ::dex::Opcode::OP_MOVE_RESULT_OBJECT
-                                             : ::dex::Opcode::OP_MOVE_RESULT,
-              RegisterValue(*instruction.dest()));
-  }
-
-  max_args_ = std::max(max_args_, instruction.args().size());
-}
-
-// Encodes a conditional branch that tests a single argument.
-void MethodBuilder::EncodeBranch(::dex::Opcode op, const Instruction& instruction) {
-  const auto& args = instruction.args();
-  const auto& test_value = args[0];
-  const auto& branch_target = args[1];
-  CHECK_EQ(2, args.size());
-  CHECK(test_value.is_variable());
-  CHECK(branch_target.is_label());
-
-  size_t instruction_offset = buffer_.size();
-  size_t field_offset = buffer_.size() + 1;
-  Encode21c(
-      op, RegisterValue(test_value), LabelValue(branch_target, instruction_offset, field_offset));
-}
-
-void MethodBuilder::EncodeNew(const Instruction& instruction) {
-  CHECK_EQ(Instruction::Op::kNew, instruction.opcode());
-  CHECK(instruction.dest().has_value());
-  CHECK(instruction.dest()->is_variable());
-  CHECK_EQ(1, instruction.args().size());
-
-  const Value& type = instruction.args()[0];
-  CHECK_LT(RegisterValue(*instruction.dest()), 256);
-  CHECK(type.is_type());
-  Encode21c(::dex::Opcode::OP_NEW_INSTANCE, RegisterValue(*instruction.dest()), type.value());
-}
-
-void MethodBuilder::EncodeCast(const Instruction& instruction) {
-  CHECK_EQ(Instruction::Op::kCheckCast, instruction.opcode());
-  CHECK(instruction.dest().has_value());
-  CHECK(instruction.dest()->is_variable());
-  CHECK_EQ(1, instruction.args().size());
-
-  const Value& type = instruction.args()[0];
-  CHECK_LT(RegisterValue(*instruction.dest()), 256);
-  CHECK(type.is_type());
-  Encode21c(::dex::Opcode::OP_CHECK_CAST, RegisterValue(*instruction.dest()), type.value());
-}
-
-void MethodBuilder::EncodeFieldOp(const Instruction& instruction) {
-  const auto& args = instruction.args();
-  switch (instruction.opcode()) {
-    case Instruction::Op::kGetStaticField: {
-      CHECK(instruction.dest().has_value());
-      CHECK(instruction.dest()->is_variable());
-      CHECK_EQ(0, instruction.args().size());
-
-      Encode21c(::dex::Opcode::OP_SGET,
-                RegisterValue(*instruction.dest()),
-                instruction.index_argument());
-      break;
-    }
-    case Instruction::Op::kSetStaticField: {
-      CHECK(!instruction.dest().has_value());
-      CHECK_EQ(1, args.size());
-      CHECK(args[0].is_variable());
-
-      Encode21c(::dex::Opcode::OP_SPUT, RegisterValue(args[0]), instruction.index_argument());
-      break;
-    }
-    case Instruction::Op::kGetInstanceField: {
-      CHECK(instruction.dest().has_value());
-      CHECK(instruction.dest()->is_variable());
-      CHECK_EQ(1, instruction.args().size());
-
-      Encode22c(::dex::Opcode::OP_IGET,
-                RegisterValue(*instruction.dest()),
-                RegisterValue(args[0]),
-                instruction.index_argument());
-      break;
-    }
-    case Instruction::Op::kSetInstanceField: {
-      CHECK(!instruction.dest().has_value());
-      CHECK_EQ(2, args.size());
-      CHECK(args[0].is_variable());
-      CHECK(args[1].is_variable());
-
-      Encode22c(::dex::Opcode::OP_IPUT,
-                RegisterValue(args[1]),
-                RegisterValue(args[0]),
-                instruction.index_argument());
-      break;
-    }
-    default: { LOG(FATAL) << "Unsupported field operation"; }
-  }
-}
-
-size_t MethodBuilder::RegisterValue(const Value& value) const {
-  if (value.is_register()) {
-    return value.value();
-  } else if (value.is_parameter()) {
-    return value.value() + NumRegisters() + kMaxScratchRegisters;
-  }
-  CHECK(false && "Must be either a parameter or a register");
-  return 0;
-}
-
-void MethodBuilder::BindLabel(const Value& label_id) {
-  CHECK(label_id.is_label());
-
-  LabelData& label = labels_[label_id.value()];
-  CHECK(!label.bound_address.has_value());
-
-  label.bound_address = buffer_.size();
-
-  // patch any forward references to this label.
-  for (const auto& ref : label.references) {
-    buffer_[ref.field_offset] = *label.bound_address - ref.instruction_offset;
-  }
-  // No point keeping these around anymore.
-  label.references.clear();
-}
-
-::dex::u2 MethodBuilder::LabelValue(const Value& label_id, size_t instruction_offset,
-                                    size_t field_offset) {
-  CHECK(label_id.is_label());
-  LabelData& label = labels_[label_id.value()];
-
-  // Short-circuit if the label is already bound.
-  if (label.bound_address.has_value()) {
-    return *label.bound_address - instruction_offset;
-  }
-
-  // Otherwise, save a reference to where we need to back-patch later.
-  label.references.push_front(LabelReference{instruction_offset, field_offset});
-  return 0;
-}
-
-const MethodDeclData& DexBuilder::GetOrDeclareMethod(TypeDescriptor type, const std::string& name,
-                                                     Prototype prototype) {
-  MethodDeclData& entry = method_id_map_[{type, name, prototype}];
-
-  if (entry.decl == nullptr) {
-    // This method has not already been declared, so declare it.
-    ir::MethodDecl* decl = dex_file_->Alloc<ir::MethodDecl>();
-    // The method id is the last added method.
-    size_t id = dex_file_->methods.size() - 1;
-
-    ir::String* dex_name{GetOrAddString(name)};
-    decl->name = dex_name;
-    decl->parent = GetOrAddType(type.descriptor());
-    decl->prototype = GetOrEncodeProto(prototype);
-
-    // update the index -> ir node map (see tools/dexter/slicer/dex_ir_builder.cc)
-    auto new_index = dex_file_->methods_indexes.AllocateIndex();
-    auto& ir_node = dex_file_->methods_map[new_index];
-    CHECK(ir_node == nullptr);
-    ir_node = decl;
-    decl->orig_index = decl->index = new_index;
-
-    entry = {id, decl};
-  }
-
-  return entry;
-}
-
-std::optional<const Prototype> DexBuilder::GetPrototypeByMethodId(size_t method_id) const {
-  for (const auto& entry : method_id_map_) {
-    if (entry.second.id == method_id) {
-      return entry.first.prototype;
-    }
-  }
-  return {};
-}
-
-ir::Proto* DexBuilder::GetOrEncodeProto(Prototype prototype) {
-  ir::Proto*& ir_proto = proto_map_[prototype];
-  if (ir_proto == nullptr) {
-    ir_proto = prototype.Encode(this);
-  }
-  return ir_proto;
-}
-
-}  // namespace dex
-}  // namespace startop
diff --git a/startop/view_compiler/dex_builder.h b/startop/view_compiler/dex_builder.h
deleted file mode 100644
index eb2dc88..0000000
--- a/startop/view_compiler/dex_builder.h
+++ /dev/null
@@ -1,626 +0,0 @@
-/*
- * Copyright (C) 2018 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.
- */
-#ifndef DEX_BUILDER_H_
-#define DEX_BUILDER_H_
-
-#include <array>
-#include <forward_list>
-#include <map>
-#include <optional>
-#include <string>
-#include <unordered_map>
-#include <vector>
-
-#include "android-base/logging.h"
-
-#include "slicer/dex_bytecode.h"
-#include "slicer/dex_ir.h"
-#include "slicer/writer.h"
-
-namespace startop {
-namespace dex {
-
-// TODO: remove this once the dex generation code is complete.
-void WriteTestDexFile(const std::string& filename);
-
-//////////////////////////
-// Forward declarations //
-//////////////////////////
-class DexBuilder;
-
-// Our custom allocator for dex::Writer
-//
-// This keeps track of all allocations and ensures they are freed when
-// TrackingAllocator is destroyed. Pointers to memory allocated by this
-// allocator must not outlive the allocator.
-class TrackingAllocator : public ::dex::Writer::Allocator {
- public:
-  virtual void* Allocate(size_t size);
-  virtual void Free(void* ptr);
-
- private:
-  std::unordered_map<void*, std::unique_ptr<uint8_t[]>> allocations_;
-};
-
-// Represents a DEX type descriptor.
-//
-// TODO: add a way to create a descriptor for a reference of a class type.
-class TypeDescriptor {
- public:
-  // Named constructors for base type descriptors.
-  static const TypeDescriptor Int();
-  static const TypeDescriptor Void();
-
-  // Creates a type descriptor from a fully-qualified class name. For example, it turns the class
-  // name java.lang.Object into the descriptor Ljava/lang/Object.
-  static TypeDescriptor FromClassname(const std::string& name);
-
-  // Return the full descriptor, such as I or Ljava/lang/Object
-  const std::string& descriptor() const { return descriptor_; }
-  // Return the shorty descriptor, such as I or L
-  std::string short_descriptor() const { return descriptor().substr(0, 1); }
-
-  bool is_object() const { return short_descriptor() == "L"; }
-
-  bool operator<(const TypeDescriptor& rhs) const { return descriptor_ < rhs.descriptor_; }
-
- private:
-  explicit TypeDescriptor(std::string descriptor) : descriptor_{descriptor} {}
-
-  const std::string descriptor_;
-};
-
-// Defines a function signature. For example, Prototype{TypeDescriptor::VOID, TypeDescriptor::Int}
-// represents the function type (Int) -> Void.
-class Prototype {
- public:
-  template <typename... TypeDescriptors>
-  explicit Prototype(TypeDescriptor return_type, TypeDescriptors... param_types)
-      : return_type_{return_type}, param_types_{param_types...} {}
-
-  // Encode this prototype into the dex file.
-  ir::Proto* Encode(DexBuilder* dex) const;
-
-  // Get the shorty descriptor, such as VII for (Int, Int) -> Void
-  std::string Shorty() const;
-
-  const TypeDescriptor& ArgType(size_t index) const;
-
-  bool operator<(const Prototype& rhs) const {
-    return std::make_tuple(return_type_, param_types_) <
-           std::make_tuple(rhs.return_type_, rhs.param_types_);
-  }
-
- private:
-  const TypeDescriptor return_type_;
-  const std::vector<TypeDescriptor> param_types_;
-};
-
-// Represents a DEX register or constant. We separate regular registers and parameters
-// because we will not know the real parameter id until after all instructions
-// have been generated.
-class Value {
- public:
-  static constexpr Value Local(size_t id) { return Value{id, Kind::kLocalRegister}; }
-  static constexpr Value Parameter(size_t id) { return Value{id, Kind::kParameter}; }
-  static constexpr Value Immediate(size_t value) { return Value{value, Kind::kImmediate}; }
-  static constexpr Value String(size_t value) { return Value{value, Kind::kString}; }
-  static constexpr Value Label(size_t id) { return Value{id, Kind::kLabel}; }
-  static constexpr Value Type(size_t id) { return Value{id, Kind::kType}; }
-
-  bool is_register() const { return kind_ == Kind::kLocalRegister; }
-  bool is_parameter() const { return kind_ == Kind::kParameter; }
-  bool is_variable() const { return is_register() || is_parameter(); }
-  bool is_immediate() const { return kind_ == Kind::kImmediate; }
-  bool is_string() const { return kind_ == Kind::kString; }
-  bool is_label() const { return kind_ == Kind::kLabel; }
-  bool is_type() const { return kind_ == Kind::kType; }
-
-  size_t value() const { return value_; }
-
-  constexpr Value() : value_{0}, kind_{Kind::kInvalid} {}
-
- private:
-  enum class Kind { kInvalid, kLocalRegister, kParameter, kImmediate, kString, kLabel, kType };
-
-  size_t value_;
-  Kind kind_;
-
-  constexpr Value(size_t value, Kind kind) : value_{value}, kind_{kind} {}
-};
-
-// Represents an allocated register returned by MethodBuilder::AllocRegister
-class LiveRegister {
-  friend class MethodBuilder;
-
- public:
-  LiveRegister(LiveRegister&& other) : liveness_{other.liveness_}, index_{other.index_} {
-    other.index_ = {};
-  };
-  ~LiveRegister() {
-    if (index_.has_value()) {
-      (*liveness_)[*index_] = false;
-    }
-  };
-
-  operator const Value() const { return Value::Local(*index_); }
-
- private:
-  LiveRegister(std::vector<bool>* liveness, size_t index) : liveness_{liveness}, index_{index} {}
-
-  std::vector<bool>* const liveness_;
-  std::optional<size_t> index_;
-};
-
-// A virtual instruction. We convert these to real instructions in MethodBuilder::Encode.
-// Virtual instructions are needed to keep track of information that is not known until all of the
-// code is generated. This information includes things like how many local registers are created and
-// branch target locations.
-class Instruction {
- public:
-  // The operation performed by this instruction. These are virtual instructions that do not
-  // correspond exactly to DEX instructions.
-  enum class Op {
-    kBindLabel,
-    kBranchEqz,
-    kBranchNEqz,
-    kCheckCast,
-    kGetInstanceField,
-    kGetStaticField,
-    kInvokeDirect,
-    kInvokeInterface,
-    kInvokeStatic,
-    kInvokeVirtual,
-    kMove,
-    kMoveObject,
-    kNew,
-    kReturn,
-    kReturnObject,
-    kSetInstanceField,
-    kSetStaticField
-  };
-
-  ////////////////////////
-  // Named Constructors //
-  ////////////////////////
-
-  // For instructions with no return value and no arguments.
-  static inline Instruction OpNoArgs(Op opcode) {
-    return Instruction{opcode, /*index_argument*/ 0, /*dest*/ {}};
-  }
-  // For most instructions, which take some number of arguments and have an optional return value.
-  template <typename... T>
-  static inline Instruction OpWithArgs(Op opcode, std::optional<const Value> dest,
-                                       const T&... args) {
-    return Instruction{opcode, /*index_argument=*/0, /*result_is_object=*/false, dest, args...};
-  }
-
-  // A cast instruction. Basically, `(type)val`
-  static inline Instruction Cast(Value val, Value type) {
-    CHECK(type.is_type());
-    return OpWithArgs(Op::kCheckCast, val, type);
-  }
-
-  // For method calls.
-  template <typename... T>
-  static inline Instruction InvokeVirtual(size_t index_argument, std::optional<const Value> dest,
-                                          Value this_arg, T... args) {
-    return Instruction{
-        Op::kInvokeVirtual, index_argument, /*result_is_object=*/false, dest, this_arg, args...};
-  }
-  // Returns an object
-  template <typename... T>
-  static inline Instruction InvokeVirtualObject(size_t index_argument,
-                                                std::optional<const Value> dest, Value this_arg,
-                                                const T&... args) {
-    return Instruction{
-        Op::kInvokeVirtual, index_argument, /*result_is_object=*/true, dest, this_arg, args...};
-  }
-  // For direct calls (basically, constructors).
-  template <typename... T>
-  static inline Instruction InvokeDirect(size_t index_argument, std::optional<const Value> dest,
-                                         Value this_arg, const T&... args) {
-    return Instruction{
-        Op::kInvokeDirect, index_argument, /*result_is_object=*/false, dest, this_arg, args...};
-  }
-  // Returns an object
-  template <typename... T>
-  static inline Instruction InvokeDirectObject(size_t index_argument,
-                                               std::optional<const Value> dest, Value this_arg,
-                                               T... args) {
-    return Instruction{
-        Op::kInvokeDirect, index_argument, /*result_is_object=*/true, dest, this_arg, args...};
-  }
-  // For static calls.
-  template <typename... T>
-  static inline Instruction InvokeStatic(size_t index_argument, std::optional<const Value> dest,
-                                         T... args) {
-    return Instruction{
-        Op::kInvokeStatic, index_argument, /*result_is_object=*/false, dest, args...};
-  }
-  // Returns an object
-  template <typename... T>
-  static inline Instruction InvokeStaticObject(size_t index_argument,
-                                               std::optional<const Value> dest, T... args) {
-    return Instruction{Op::kInvokeStatic, index_argument, /*result_is_object=*/true, dest, args...};
-  }
-  // For static calls.
-  template <typename... T>
-  static inline Instruction InvokeInterface(size_t index_argument, std::optional<const Value> dest,
-                                            const T&... args) {
-    return Instruction{
-        Op::kInvokeInterface, index_argument, /*result_is_object=*/false, dest, args...};
-  }
-
-  static inline Instruction GetStaticField(size_t field_id, Value dest) {
-    return Instruction{Op::kGetStaticField, field_id, dest};
-  }
-
-  static inline Instruction SetStaticField(size_t field_id, Value value) {
-    return Instruction{
-        Op::kSetStaticField, field_id, /*result_is_object=*/false, /*dest=*/{}, value};
-  }
-
-  static inline Instruction GetField(size_t field_id, Value dest, Value object) {
-    return Instruction{Op::kGetInstanceField, field_id, /*result_is_object=*/false, dest, object};
-  }
-
-  static inline Instruction SetField(size_t field_id, Value object, Value value) {
-    return Instruction{
-        Op::kSetInstanceField, field_id, /*result_is_object=*/false, /*dest=*/{}, object, value};
-  }
-
-  ///////////////
-  // Accessors //
-  ///////////////
-
-  Op opcode() const { return opcode_; }
-  size_t index_argument() const { return index_argument_; }
-  bool result_is_object() const { return result_is_object_; }
-  const std::optional<const Value>& dest() const { return dest_; }
-  const std::vector<const Value>& args() const { return args_; }
-
- private:
-  inline Instruction(Op opcode, size_t index_argument, std::optional<const Value> dest)
-      : opcode_{opcode},
-        index_argument_{index_argument},
-        result_is_object_{false},
-        dest_{dest},
-        args_{} {}
-
-  template <typename... T>
-  inline Instruction(Op opcode, size_t index_argument, bool result_is_object,
-                     std::optional<const Value> dest, const T&... args)
-      : opcode_{opcode},
-        index_argument_{index_argument},
-        result_is_object_{result_is_object},
-        dest_{dest},
-        args_{args...} {}
-
-  const Op opcode_;
-  // The index of the method to invoke, for kInvokeVirtual and similar opcodes.
-  const size_t index_argument_{0};
-  const bool result_is_object_;
-  const std::optional<const Value> dest_;
-  const std::vector<const Value> args_;
-};
-
-// Needed for CHECK_EQ, DCHECK_EQ, etc.
-std::ostream& operator<<(std::ostream& out, const Instruction::Op& opcode);
-
-// Keeps track of information needed to manipulate or call a method.
-struct MethodDeclData {
-  size_t id;
-  ir::MethodDecl* decl;
-};
-
-// Tools to help build methods and their bodies.
-class MethodBuilder {
- public:
-  MethodBuilder(DexBuilder* dex, ir::Class* class_def, ir::MethodDecl* decl);
-
-  // Encode the method into DEX format.
-  ir::EncodedMethod* Encode();
-
-  // Create a new register to be used to storing values.
-  LiveRegister AllocRegister();
-
-  Value MakeLabel();
-
-  /////////////////////////////////
-  // Instruction builder methods //
-  /////////////////////////////////
-
-  void AddInstruction(Instruction instruction);
-
-  // return-void
-  void BuildReturn();
-  void BuildReturn(Value src, bool is_object = false);
-  // const/4
-  void BuildConst4(Value target, int value);
-  void BuildConstString(Value target, const std::string& value);
-  template <typename... T>
-  void BuildNew(Value target, TypeDescriptor type, Prototype constructor, const T&... args);
-
-  // TODO: add builders for more instructions
-
-  DexBuilder* dex_file() const { return dex_; }
-
- private:
-  void EncodeInstructions();
-  void EncodeInstruction(const Instruction& instruction);
-
-  // Encodes a return instruction. For instructions with no return value, the opcode field is
-  // ignored. Otherwise, this specifies which return instruction will be used (return,
-  // return-object, etc.)
-  void EncodeReturn(const Instruction& instruction, ::dex::Opcode opcode);
-
-  void EncodeMove(const Instruction& instruction);
-  void EncodeInvoke(const Instruction& instruction, ::dex::Opcode opcode);
-  void EncodeBranch(::dex::Opcode op, const Instruction& instruction);
-  void EncodeNew(const Instruction& instruction);
-  void EncodeCast(const Instruction& instruction);
-  void EncodeFieldOp(const Instruction& instruction);
-
-  // Low-level instruction format encoding. See
-  // https://source.android.com/devices/tech/dalvik/instruction-formats for documentation of
-  // formats.
-
-  inline uint8_t ToBits(::dex::Opcode opcode) {
-    static_assert(sizeof(uint8_t) == sizeof(::dex::Opcode));
-    return static_cast<uint8_t>(opcode);
-  }
-
-  inline void Encode10x(::dex::Opcode opcode) {
-    // 00|op
-    static_assert(sizeof(uint8_t) == sizeof(::dex::Opcode));
-    buffer_.push_back(ToBits(opcode));
-  }
-
-  inline void Encode11x(::dex::Opcode opcode, uint8_t a) {
-    // aa|op
-    buffer_.push_back((a << 8) | ToBits(opcode));
-  }
-
-  inline void Encode11n(::dex::Opcode opcode, uint8_t a, int8_t b) {
-    // b|a|op
-
-    // Make sure the fields are in bounds (4 bits for a, 4 bits for b).
-    CHECK_LT(a, 16);
-    CHECK_LE(-8, b);
-    CHECK_LT(b, 8);
-
-    buffer_.push_back(((b & 0xf) << 12) | (a << 8) | ToBits(opcode));
-  }
-
-  inline void Encode21c(::dex::Opcode opcode, uint8_t a, uint16_t b) {
-    // aa|op|bbbb
-    buffer_.push_back((a << 8) | ToBits(opcode));
-    buffer_.push_back(b);
-  }
-
-  inline void Encode22c(::dex::Opcode opcode, uint8_t a, uint8_t b, uint16_t c) {
-    // b|a|op|bbbb
-    CHECK(IsShortRegister(a));
-    CHECK(IsShortRegister(b));
-    buffer_.push_back((b << 12) | (a << 8) | ToBits(opcode));
-    buffer_.push_back(c);
-  }
-
-  inline void Encode32x(::dex::Opcode opcode, uint16_t a, uint16_t b) {
-    buffer_.push_back(ToBits(opcode));
-    buffer_.push_back(a);
-    buffer_.push_back(b);
-  }
-
-  inline void Encode35c(::dex::Opcode opcode, size_t a, uint16_t b, uint8_t c, uint8_t d,
-                        uint8_t e, uint8_t f, uint8_t g) {
-    // a|g|op|bbbb|f|e|d|c
-
-    CHECK_LE(a, 5);
-    CHECK(IsShortRegister(c));
-    CHECK(IsShortRegister(d));
-    CHECK(IsShortRegister(e));
-    CHECK(IsShortRegister(f));
-    CHECK(IsShortRegister(g));
-    buffer_.push_back((a << 12) | (g << 8) | ToBits(opcode));
-    buffer_.push_back(b);
-    buffer_.push_back((f << 12) | (e << 8) | (d << 4) | c);
-  }
-
-  inline void Encode3rc(::dex::Opcode opcode, size_t a, uint16_t b, uint16_t c) {
-    CHECK_LE(a, 255);
-    buffer_.push_back((a << 8) | ToBits(opcode));
-    buffer_.push_back(b);
-    buffer_.push_back(c);
-  }
-
-  static constexpr bool IsShortRegister(size_t register_value) { return register_value < 16; }
-
-  // Returns an array of num_regs scratch registers. These are guaranteed to be
-  // contiguous, so they are suitable for the invoke-*/range instructions.
-  template <int num_regs>
-  std::array<Value, num_regs> GetScratchRegisters() const {
-    static_assert(num_regs <= kMaxScratchRegisters);
-    std::array<Value, num_regs> regs;
-    for (size_t i = 0; i < num_regs; ++i) {
-      regs[i] = std::move(Value::Local(NumRegisters() + i));
-    }
-    return regs;
-  }
-
-  // Converts a register or parameter to its DEX register number.
-  size_t RegisterValue(const Value& value) const;
-
-  // Sets a label's address to the current position in the instruction buffer. If there are any
-  // forward references to the label, this function will back-patch them.
-  void BindLabel(const Value& label);
-
-  // Returns the offset of the label relative to the given instruction offset. If the label is not
-  // bound, a reference will be saved and it will automatically be patched when the label is bound.
-  ::dex::u2 LabelValue(const Value& label, size_t instruction_offset, size_t field_offset);
-
-  DexBuilder* dex_;
-  ir::Class* class_;
-  ir::MethodDecl* decl_;
-
-  // A list of the instructions we will eventually encode.
-  std::vector<Instruction> instructions_;
-
-  // A buffer to hold instructions that have been encoded.
-  std::vector<::dex::u2> buffer_;
-
-  // We create some scratch registers for when we have to shuffle registers
-  // around to make legal DEX code.
-  static constexpr size_t kMaxScratchRegisters = 5;
-
-  size_t NumRegisters() const {
-    return register_liveness_.size();
-  }
-
-  // Stores information needed to back-patch a label once it is bound. We need to know the start of
-  // the instruction that refers to the label, and the offset to where the actual label value should
-  // go.
-  struct LabelReference {
-    size_t instruction_offset;
-    size_t field_offset;
-  };
-
-  struct LabelData {
-    std::optional<size_t> bound_address;
-    std::forward_list<LabelReference> references;
-  };
-
-  std::vector<LabelData> labels_;
-
-  // During encoding, keep track of the largest number of arguments needed, so we can use it for our
-  // outs count
-  size_t max_args_{0};
-
-  std::vector<bool> register_liveness_;
-};
-
-// A helper to build class definitions.
-class ClassBuilder {
- public:
-  ClassBuilder(DexBuilder* parent, const std::string& name, ir::Class* class_def);
-
-  void set_source_file(const std::string& source);
-
-  // Create a method with the given name and prototype. The returned MethodBuilder can be used to
-  // fill in the method body.
-  MethodBuilder CreateMethod(const std::string& name, Prototype prototype);
-
- private:
-  DexBuilder* const parent_;
-  const TypeDescriptor type_descriptor_;
-  ir::Class* const class_;
-};
-
-// Builds Dex files from scratch.
-class DexBuilder {
- public:
-  DexBuilder();
-
-  // Create an in-memory image of the DEX file that can either be loaded directly or written to a
-  // file.
-  slicer::MemView CreateImage();
-
-  template <typename T>
-  T* Alloc() {
-    return dex_file_->Alloc<T>();
-  }
-
-  // Find the ir::String that matches the given string, creating it if it does not exist.
-  ir::String* GetOrAddString(const std::string& string);
-  // Create a new class of the given name.
-  ClassBuilder MakeClass(const std::string& name);
-
-  // Add a type for the given descriptor, or return the existing one if it already exists.
-  // See the TypeDescriptor class for help generating these. GetOrAddType can be used to declare
-  // imported classes.
-  ir::Type* GetOrAddType(const std::string& descriptor);
-  inline ir::Type* GetOrAddType(TypeDescriptor descriptor) {
-    return GetOrAddType(descriptor.descriptor());
-  }
-
-  ir::FieldDecl* GetOrAddField(TypeDescriptor parent, const std::string& name, TypeDescriptor type);
-
-  // Returns the method id for the method, creating it if it has not been created yet.
-  const MethodDeclData& GetOrDeclareMethod(TypeDescriptor type, const std::string& name,
-                                           Prototype prototype);
-
-  std::optional<const Prototype> GetPrototypeByMethodId(size_t method_id) const;
-
- private:
-  // Looks up the ir::Proto* corresponding to this given prototype, or creates one if it does not
-  // exist.
-  ir::Proto* GetOrEncodeProto(Prototype prototype);
-
-  std::shared_ptr<ir::DexFile> dex_file_;
-
-  // allocator_ is needed to be able to encode the image.
-  TrackingAllocator allocator_;
-
-  // We'll need to allocate buffers for all of the encoded strings we create. This is where we store
-  // all of them.
-  std::vector<std::unique_ptr<uint8_t[]>> string_data_;
-
-  // Keep track of what types we've defined so we can look them up later.
-  std::unordered_map<std::string, ir::Type*> types_by_descriptor_;
-
-  struct MethodDescriptor {
-    TypeDescriptor type;
-    std::string name;
-    Prototype prototype;
-
-    inline bool operator<(const MethodDescriptor& rhs) const {
-      return std::make_tuple(type, name, prototype) <
-             std::make_tuple(rhs.type, rhs.name, rhs.prototype);
-    }
-  };
-
-  // Maps method declarations to their method index. This is needed to encode references to them.
-  // When we go to actually write the DEX file, slicer will re-assign these after correctly sorting
-  // the methods list.
-  std::map<MethodDescriptor, MethodDeclData> method_id_map_;
-
-  // Keep track of what strings we've defined so we can look them up later.
-  std::unordered_map<std::string, ir::String*> strings_;
-
-  // Keep track of already-encoded protos.
-  std::map<Prototype, ir::Proto*> proto_map_;
-
-  // Keep track of fields that have been declared
-  std::map<std::tuple<TypeDescriptor, std::string>, ir::FieldDecl*> field_decls_by_key_;
-};
-
-template <typename... T>
-void MethodBuilder::BuildNew(Value target, TypeDescriptor type, Prototype constructor,
-                             const T&... args) {
-  MethodDeclData constructor_data{dex_->GetOrDeclareMethod(type, "<init>", constructor)};
-  // allocate the object
-  ir::Type* type_def = dex_->GetOrAddType(type.descriptor());
-  AddInstruction(
-      Instruction::OpWithArgs(Instruction::Op::kNew, target, Value::Type(type_def->orig_index)));
-  // call the constructor
-  AddInstruction(Instruction::InvokeDirect(constructor_data.id, /*dest=*/{}, target, args...));
-};
-
-}  // namespace dex
-}  // namespace startop
-
-#endif  // DEX_BUILDER_H_
diff --git a/startop/view_compiler/dex_builder_test/Android.bp b/startop/view_compiler/dex_builder_test/Android.bp
deleted file mode 100644
index bcba2fe..0000000
--- a/startop/view_compiler/dex_builder_test/Android.bp
+++ /dev/null
@@ -1,68 +0,0 @@
-//
-// Copyright (C) 2018 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 {
-    // 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"],
-}
-
-genrule {
-    name: "generate_compiled_layout1",
-    tools: [":viewcompiler"],
-    cmd: "$(location :viewcompiler) $(in) --dex --out $(out) --package android.startop.test",
-    srcs: ["res/layout/layout1.xml"],
-    out: [
-        "layout1.dex",
-    ],
-}
-
-genrule {
-    name: "generate_compiled_layout2",
-    tools: [":viewcompiler"],
-    cmd: "$(location :viewcompiler) $(in) --dex --out $(out) --package android.startop.test",
-    srcs: ["res/layout/layout2.xml"],
-    out: [
-        "layout2.dex",
-    ],
-}
-
-android_test {
-    name: "dex-builder-test",
-    srcs: [
-        "src/android/startop/test/DexBuilderTest.java",
-        "src/android/startop/test/LayoutCompilerTest.java",
-        "src/android/startop/test/TestClass.java",
-    ],
-    sdk_version: "current",
-    data: [
-        ":generate_dex_testcases",
-        ":generate_compiled_layout1",
-        ":generate_compiled_layout2",
-    ],
-    static_libs: [
-        "androidx.test.core",
-        "androidx.test.runner",
-        "junit",
-    ],
-    manifest: "AndroidManifest.xml",
-    resource_dirs: ["res"],
-    test_config: "AndroidTest.xml",
-    test_suites: ["general-tests"],
-}
diff --git a/startop/view_compiler/dex_builder_test/AndroidManifest.xml b/startop/view_compiler/dex_builder_test/AndroidManifest.xml
deleted file mode 100644
index b335663..0000000
--- a/startop/view_compiler/dex_builder_test/AndroidManifest.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 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="android.startop.test" >
-
-    <uses-sdk android:minSdkVersion="28" android:targetSdkVersion="28" />
-
-    <application>
-        <uses-library android:name="android.test.runner" />
-    </application>
-
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="android.startop.test"
-                     android:label="DexBuilder Tests"/>
-
-</manifest>
diff --git a/startop/view_compiler/dex_builder_test/AndroidTest.xml b/startop/view_compiler/dex_builder_test/AndroidTest.xml
deleted file mode 100644
index 59093c7..0000000
--- a/startop/view_compiler/dex_builder_test/AndroidTest.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<configuration description="Runs DexBuilder Tests.">
-    <option name="test-suite-tag" value="apct" />
-    <option name="test-suite-tag" value="apct-instrumentation" />
-    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
-        <option name="cleanup-apks" value="true" />
-        <option name="test-file-name" value="dex-builder-test.apk" />
-    </target_preparer>
-
-    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
-        <option name="cleanup" value="true" />
-        <option name="push" value="trivial.dex->/data/local/tmp/dex-builder-test/trivial.dex" />
-        <option name="push" value="simple.dex->/data/local/tmp/dex-builder-test/simple.dex" />
-        <option name="push" value="layout1.dex->/data/local/tmp/dex-builder-test/layout1.dex" />
-        <option name="push" value="layout2.dex->/data/local/tmp/dex-builder-test/layout2.dex" />
-    </target_preparer>
-
-    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
-        <option name="package" value="android.startop.test" />
-        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
-    </test>
-</configuration>
diff --git a/startop/view_compiler/dex_builder_test/res/layout/layout1.xml b/startop/view_compiler/dex_builder_test/res/layout/layout1.xml
deleted file mode 100644
index 0f9375c..0000000
--- a/startop/view_compiler/dex_builder_test/res/layout/layout1.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-   android:layout_width="match_parent"
-   android:layout_height="match_parent"
-   android:paddingLeft="16dp"
-   android:paddingRight="16dp"
-   android:orientation="vertical"
-   android:gravity="center">
-
-    <Button
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"/>
-    <Button
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"/>
-
- </LinearLayout>
diff --git a/startop/view_compiler/dex_builder_test/res/layout/layout2.xml b/startop/view_compiler/dex_builder_test/res/layout/layout2.xml
deleted file mode 100644
index b092e1c..0000000
--- a/startop/view_compiler/dex_builder_test/res/layout/layout2.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-
-    <TableRow
-        android:layout_width="match_parent"
-        android:layout_height="match_parent" >
-
-        <Button
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="Button" />
-
-        <TableRow
-            android:layout_width="match_parent"
-            android:layout_height="match_parent" >
-
-            <Button
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:text="Button" />
-            <TableRow
-                android:layout_width="match_parent"
-                android:layout_height="match_parent" >
-
-                <Button
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:text="Button" />
-
-                <TableRow
-                    android:layout_width="match_parent"
-                    android:layout_height="match_parent" >
-
-                    <Button
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
-                        android:text="Button" />
-                </TableRow>
-
-            </TableRow>
-        </TableRow>
-    </TableRow>
-</LinearLayout>
diff --git a/startop/view_compiler/dex_builder_test/src/android/startop/test/DexBuilderTest.java b/startop/view_compiler/dex_builder_test/src/android/startop/test/DexBuilderTest.java
deleted file mode 100644
index 6af01f6f..0000000
--- a/startop/view_compiler/dex_builder_test/src/android/startop/test/DexBuilderTest.java
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * Copyright (C) 2018 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.startop.test;
-
-import android.content.Context;
-import androidx.test.InstrumentationRegistry;
-import dalvik.system.PathClassLoader;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import org.junit.Assert;
-import org.junit.Test;
-
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-
-// Adding tests here requires changes in several other places. See README.md in
-// the view_compiler directory for more information.
-public final class DexBuilderTest {
-  static ClassLoader loadDexFile(String filename) throws Exception {
-    return new PathClassLoader("/data/local/tmp/dex-builder-test/" + filename,
-        DexBuilderTest.class.getClassLoader());
-  }
-
-  public void hello() {}
-
-  @Test
-  public void loadTrivialDex() throws Exception {
-    ClassLoader loader = loadDexFile("trivial.dex");
-    loader.loadClass("android.startop.test.testcases.Trivial");
-  }
-
-  @Test
-  public void return5() throws Exception {
-    ClassLoader loader = loadDexFile("simple.dex");
-    Class clazz = loader.loadClass("android.startop.test.testcases.SimpleTests");
-    Method method = clazz.getMethod("return5");
-    Assert.assertEquals(5, method.invoke(null));
-  }
-
-  @Test
-  public void returnInteger5() throws Exception {
-    ClassLoader loader = loadDexFile("simple.dex");
-    Class clazz = loader.loadClass("android.startop.test.testcases.SimpleTests");
-    Method method = clazz.getMethod("returnInteger5");
-    Assert.assertEquals(5, method.invoke(null));
-  }
-
-  @Test
-  public void returnParam() throws Exception {
-    ClassLoader loader = loadDexFile("simple.dex");
-    Class clazz = loader.loadClass("android.startop.test.testcases.SimpleTests");
-    Method method = clazz.getMethod("returnParam", int.class);
-    Assert.assertEquals(5, method.invoke(null, 5));
-    Assert.assertEquals(42, method.invoke(null, 42));
-  }
-
-  @Test
-  public void returnStringLength() throws Exception {
-    ClassLoader loader = loadDexFile("simple.dex");
-    Class clazz = loader.loadClass("android.startop.test.testcases.SimpleTests");
-    Method method = clazz.getMethod("returnStringLength", String.class);
-    Assert.assertEquals(13, method.invoke(null, "Hello, World!"));
-  }
-
-  @Test
-  public void returnIfZero() throws Exception {
-    ClassLoader loader = loadDexFile("simple.dex");
-    Class clazz = loader.loadClass("android.startop.test.testcases.SimpleTests");
-    Method method = clazz.getMethod("returnIfZero", int.class);
-    Assert.assertEquals(5, method.invoke(null, 0));
-    Assert.assertEquals(3, method.invoke(null, 17));
-  }
-
-  @Test
-  public void returnIfNotZero() throws Exception {
-    ClassLoader loader = loadDexFile("simple.dex");
-    Class clazz = loader.loadClass("android.startop.test.testcases.SimpleTests");
-    Method method = clazz.getMethod("returnIfNotZero", int.class);
-    Assert.assertEquals(3, method.invoke(null, 0));
-    Assert.assertEquals(5, method.invoke(null, 17));
-  }
-
-  @Test
-  public void backwardsBranch() throws Exception {
-    ClassLoader loader = loadDexFile("simple.dex");
-    Class clazz = loader.loadClass("android.startop.test.testcases.SimpleTests");
-    Method method = clazz.getMethod("backwardsBranch");
-    Assert.assertEquals(2, method.invoke(null));
-  }
-
-  @Test
-  public void returnNull() throws Exception {
-    ClassLoader loader = loadDexFile("simple.dex");
-    Class clazz = loader.loadClass("android.startop.test.testcases.SimpleTests");
-    Method method = clazz.getMethod("returnNull");
-    Assert.assertEquals(null, method.invoke(null));
-  }
-
-  @Test
-  public void makeString() throws Exception {
-    ClassLoader loader = loadDexFile("simple.dex");
-    Class clazz = loader.loadClass("android.startop.test.testcases.SimpleTests");
-    Method method = clazz.getMethod("makeString");
-    Assert.assertEquals("Hello, World!", method.invoke(null));
-  }
-
-  @Test
-  public void returnStringIfZeroAB() throws Exception {
-    ClassLoader loader = loadDexFile("simple.dex");
-    Class clazz = loader.loadClass("android.startop.test.testcases.SimpleTests");
-    Method method = clazz.getMethod("returnStringIfZeroAB", int.class);
-    Assert.assertEquals("a", method.invoke(null, 0));
-    Assert.assertEquals("b", method.invoke(null, 1));
-  }
-
-  @Test
-  public void returnStringIfZeroBA() throws Exception {
-    ClassLoader loader = loadDexFile("simple.dex");
-    Class clazz = loader.loadClass("android.startop.test.testcases.SimpleTests");
-    Method method = clazz.getMethod("returnStringIfZeroBA", int.class);
-    Assert.assertEquals("b", method.invoke(null, 0));
-    Assert.assertEquals("a", method.invoke(null, 1));
-  }
-
-  @Test
-  public void invokeStaticReturnObject() throws Exception {
-    ClassLoader loader = loadDexFile("simple.dex");
-    Class clazz = loader.loadClass("android.startop.test.testcases.SimpleTests");
-    Method method = clazz.getMethod("invokeStaticReturnObject", int.class, int.class);
-    Assert.assertEquals("10", method.invoke(null, 10, 10));
-    Assert.assertEquals("a", method.invoke(null, 10, 16));
-    Assert.assertEquals("5", method.invoke(null, 5, 16));
-  }
-
-  @Test
-  public void invokeVirtualReturnObject() throws Exception {
-    ClassLoader loader = loadDexFile("simple.dex");
-    Class clazz = loader.loadClass("android.startop.test.testcases.SimpleTests");
-    Method method = clazz.getMethod("invokeVirtualReturnObject", String.class, int.class);
-    Assert.assertEquals("bc", method.invoke(null, "abc", 1));
-  }
-
-  @Test
-  public void castObjectToString() throws Exception {
-    ClassLoader loader = loadDexFile("simple.dex");
-    Class clazz = loader.loadClass("android.startop.test.testcases.SimpleTests");
-    Method method = clazz.getMethod("castObjectToString", Object.class);
-    Assert.assertEquals("abc", method.invoke(null, "abc"));
-    boolean castFailed = false;
-    try {
-      method.invoke(null, 5);
-    } catch (InvocationTargetException e) {
-      if (e.getCause() instanceof ClassCastException) {
-        castFailed = true;
-      } else {
-        throw e;
-      }
-    }
-    Assert.assertTrue(castFailed);
-  }
-
-  @Test
-  public void readStaticField() throws Exception {
-    ClassLoader loader = loadDexFile("simple.dex");
-    Class clazz = loader.loadClass("android.startop.test.testcases.SimpleTests");
-    Method method = clazz.getMethod("readStaticField");
-    TestClass.staticInteger = 5;
-    Assert.assertEquals(5, method.invoke(null));
-  }
-
-  @Test
-  public void setStaticField() throws Exception {
-    ClassLoader loader = loadDexFile("simple.dex");
-    Class clazz = loader.loadClass("android.startop.test.testcases.SimpleTests");
-    Method method = clazz.getMethod("setStaticField");
-    TestClass.staticInteger = 5;
-    method.invoke(null);
-    Assert.assertEquals(7, TestClass.staticInteger);
-  }
-
-  @Test
-  public void readInstanceField() throws Exception {
-    ClassLoader loader = loadDexFile("simple.dex");
-    Class clazz = loader.loadClass("android.startop.test.testcases.SimpleTests");
-    Method method = clazz.getMethod("readInstanceField", TestClass.class);
-    TestClass obj = new TestClass();
-    obj.instanceField = 5;
-    Assert.assertEquals(5, method.invoke(null, obj));
-  }
-
-  @Test
-  public void setInstanceField() throws Exception {
-    ClassLoader loader = loadDexFile("simple.dex");
-    Class clazz = loader.loadClass("android.startop.test.testcases.SimpleTests");
-    Method method = clazz.getMethod("setInstanceField", TestClass.class);
-    TestClass obj = new TestClass();
-    obj.instanceField = 5;
-    method.invoke(null, obj);
-    Assert.assertEquals(7, obj.instanceField);
-  }
-}
diff --git a/startop/view_compiler/dex_builder_test/src/android/startop/test/LayoutCompilerTest.java b/startop/view_compiler/dex_builder_test/src/android/startop/test/LayoutCompilerTest.java
deleted file mode 100644
index b0cf91d..0000000
--- a/startop/view_compiler/dex_builder_test/src/android/startop/test/LayoutCompilerTest.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2018 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.startop.test;
-
-import android.content.Context;
-import androidx.test.InstrumentationRegistry;
-import android.view.View;
-import dalvik.system.PathClassLoader;
-import java.lang.reflect.Method;
-import org.junit.Assert;
-import org.junit.Test;
-
-import java.lang.reflect.Method;
-
-// Adding tests here requires changes in several other places. See README.md in
-// the view_compiler directory for more information.
-public class LayoutCompilerTest {
-    static ClassLoader loadDexFile(String filename) throws Exception {
-        return new PathClassLoader("/data/local/tmp/dex-builder-test/" + filename,
-                ClassLoader.getSystemClassLoader());
-    }
-
-    @Test
-    public void loadAndInflateLayout1() throws Exception {
-        ClassLoader dex_file = loadDexFile("layout1.dex");
-        Class compiled_view = dex_file.loadClass("android.startop.test.CompiledView");
-        Method layout1 = compiled_view.getMethod("layout1", Context.class, int.class);
-        Context context = InstrumentationRegistry.getTargetContext();
-        layout1.invoke(null, context, R.layout.layout1);
-    }
-
-    @Test
-    public void loadAndInflateLayout2() throws Exception {
-        ClassLoader dex_file = loadDexFile("layout2.dex");
-        Class compiled_view = dex_file.loadClass("android.startop.test.CompiledView");
-        Method layout1 = compiled_view.getMethod("layout2", Context.class, int.class);
-        Context context = InstrumentationRegistry.getTargetContext();
-        layout1.invoke(null, context, R.layout.layout1);
-    }
-}
diff --git a/startop/view_compiler/dex_builder_test/src/android/startop/test/TestClass.java b/startop/view_compiler/dex_builder_test/src/android/startop/test/TestClass.java
deleted file mode 100644
index dd77923..0000000
--- a/startop/view_compiler/dex_builder_test/src/android/startop/test/TestClass.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2018 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.startop.test;
-
- /**
- * A simple class to help test DexBuilder.
- */
-public final class TestClass {
-    public static int staticInteger;
-
-    public int instanceField;
-}
diff --git a/startop/view_compiler/dex_layout_compiler.cc b/startop/view_compiler/dex_layout_compiler.cc
deleted file mode 100644
index bddb8aa..0000000
--- a/startop/view_compiler/dex_layout_compiler.cc
+++ /dev/null
@@ -1,208 +0,0 @@
-/*
- * Copyright (C) 2018 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.
- */
-
-#include "dex_layout_compiler.h"
-#include "layout_validation.h"
-
-#include "android-base/stringprintf.h"
-
-namespace startop {
-
-using android::base::StringPrintf;
-using dex::Instruction;
-using dex::LiveRegister;
-using dex::Prototype;
-using dex::TypeDescriptor;
-using dex::Value;
-
-namespace {
-// TODO: these are a bunch of static initializers, which we should avoid. See if
-// we can make them constexpr.
-const TypeDescriptor kAttributeSet = TypeDescriptor::FromClassname("android.util.AttributeSet");
-const TypeDescriptor kContext = TypeDescriptor::FromClassname("android.content.Context");
-const TypeDescriptor kLayoutInflater = TypeDescriptor::FromClassname("android.view.LayoutInflater");
-const TypeDescriptor kResources = TypeDescriptor::FromClassname("android.content.res.Resources");
-const TypeDescriptor kString = TypeDescriptor::FromClassname("java.lang.String");
-const TypeDescriptor kView = TypeDescriptor::FromClassname("android.view.View");
-const TypeDescriptor kViewGroup = TypeDescriptor::FromClassname("android.view.ViewGroup");
-const TypeDescriptor kXmlResourceParser =
-    TypeDescriptor::FromClassname("android.content.res.XmlResourceParser");
-}  // namespace
-
-DexViewBuilder::DexViewBuilder(dex::MethodBuilder* method)
-    : method_{method},
-      context_{Value::Parameter(0)},
-      resid_{Value::Parameter(1)},
-      inflater_{method->AllocRegister()},
-      xml_{method->AllocRegister()},
-      attrs_{method->AllocRegister()},
-      classname_tmp_{method->AllocRegister()},
-      xml_next_{method->dex_file()->GetOrDeclareMethod(kXmlResourceParser, "next",
-                                                       Prototype{TypeDescriptor::Int()})},
-      try_create_view_{method->dex_file()->GetOrDeclareMethod(
-          kLayoutInflater, "tryCreateView",
-          Prototype{kView, kView, kString, kContext, kAttributeSet})},
-      generate_layout_params_{method->dex_file()->GetOrDeclareMethod(
-          kViewGroup, "generateLayoutParams",
-          Prototype{TypeDescriptor::FromClassname("android.view.ViewGroup$LayoutParams"),
-                    kAttributeSet})},
-      add_view_{method->dex_file()->GetOrDeclareMethod(
-          kViewGroup, "addView",
-          Prototype{TypeDescriptor::Void(),
-                    kView,
-                    TypeDescriptor::FromClassname("android.view.ViewGroup$LayoutParams")})} {}
-
-void DexViewBuilder::BuildGetLayoutInflater(Value dest) {
-  // dest = LayoutInflater.from(context);
-  auto layout_inflater_from = method_->dex_file()->GetOrDeclareMethod(
-      kLayoutInflater, "from", Prototype{kLayoutInflater, kContext});
-  method_->AddInstruction(Instruction::InvokeStaticObject(layout_inflater_from.id, dest, context_));
-}
-
-void DexViewBuilder::BuildGetResources(Value dest) {
-  // dest = context.getResources();
-  auto get_resources =
-      method_->dex_file()->GetOrDeclareMethod(kContext, "getResources", Prototype{kResources});
-  method_->AddInstruction(Instruction::InvokeVirtualObject(get_resources.id, dest, context_));
-}
-
-void DexViewBuilder::BuildGetLayoutResource(Value dest, Value resources, Value resid) {
-  // dest = resources.getLayout(resid);
-  auto get_layout = method_->dex_file()->GetOrDeclareMethod(
-      kResources, "getLayout", Prototype{kXmlResourceParser, TypeDescriptor::Int()});
-  method_->AddInstruction(Instruction::InvokeVirtualObject(get_layout.id, dest, resources, resid));
-}
-
-void DexViewBuilder::BuildLayoutResourceToAttributeSet(dex::Value dest,
-                                                       dex::Value layout_resource) {
-  // dest = Xml.asAttributeSet(layout_resource);
-  auto as_attribute_set = method_->dex_file()->GetOrDeclareMethod(
-      TypeDescriptor::FromClassname("android.util.Xml"),
-      "asAttributeSet",
-      Prototype{kAttributeSet, TypeDescriptor::FromClassname("org.xmlpull.v1.XmlPullParser")});
-  method_->AddInstruction(
-      Instruction::InvokeStaticObject(as_attribute_set.id, dest, layout_resource));
-}
-
-void DexViewBuilder::BuildXmlNext() {
-  // xml_.next();
-  method_->AddInstruction(Instruction::InvokeInterface(xml_next_.id, {}, xml_));
-}
-
-void DexViewBuilder::Start() {
-  BuildGetLayoutInflater(/*dest=*/inflater_);
-  BuildGetResources(/*dest=*/xml_);
-  BuildGetLayoutResource(/*dest=*/xml_, /*resources=*/xml_, resid_);
-  BuildLayoutResourceToAttributeSet(/*dest=*/attrs_, /*layout_resource=*/xml_);
-
-  // Advance past start document tag
-  BuildXmlNext();
-}
-
-void DexViewBuilder::Finish() {}
-
-namespace {
-std::string ResolveName(const std::string& name) {
-  if (name == "View") return "android.view.View";
-  if (name == "ViewGroup") return "android.view.ViewGroup";
-  if (name.find('.') == std::string::npos) {
-    return StringPrintf("android.widget.%s", name.c_str());
-  }
-  return name;
-}
-}  // namespace
-
-void DexViewBuilder::BuildTryCreateView(Value dest, Value parent, Value classname) {
-  // dest = inflater_.tryCreateView(parent, classname, context_, attrs_);
-  method_->AddInstruction(Instruction::InvokeVirtualObject(
-      try_create_view_.id, dest, inflater_, parent, classname, context_, attrs_));
-}
-
-void DexViewBuilder::StartView(const std::string& name, bool is_viewgroup) {
-  bool const is_root_view = view_stack_.empty();
-
-  // Advance to start tag
-  BuildXmlNext();
-
-  LiveRegister view = AcquireRegister();
-  // try to create the view using the factories
-  method_->BuildConstString(classname_tmp_,
-                            name);  // TODO: the need to fully qualify the classname
-  if (is_root_view) {
-    LiveRegister null = AcquireRegister();
-    method_->BuildConst4(null, 0);
-    BuildTryCreateView(/*dest=*/view, /*parent=*/null, classname_tmp_);
-  } else {
-    BuildTryCreateView(/*dest=*/view, /*parent=*/GetCurrentView(), classname_tmp_);
-  }
-  auto label = method_->MakeLabel();
-  // branch if not null
-  method_->AddInstruction(
-      Instruction::OpWithArgs(Instruction::Op::kBranchNEqz, /*dest=*/{}, view, label));
-
-  // If null, create the class directly.
-  method_->BuildNew(view,
-                    TypeDescriptor::FromClassname(ResolveName(name)),
-                    Prototype{TypeDescriptor::Void(), kContext, kAttributeSet},
-                    context_,
-                    attrs_);
-
-  method_->AddInstruction(Instruction::OpWithArgs(Instruction::Op::kBindLabel, /*dest=*/{}, label));
-
-  if (is_viewgroup) {
-    // Cast to a ViewGroup so we can add children later.
-    const ir::Type* view_group_def = method_->dex_file()->GetOrAddType(kViewGroup.descriptor());
-    method_->AddInstruction(Instruction::Cast(view, Value::Type(view_group_def->orig_index)));
-  }
-
-  if (!is_root_view) {
-    // layout_params = parent.generateLayoutParams(attrs);
-    LiveRegister layout_params{AcquireRegister()};
-    method_->AddInstruction(Instruction::InvokeVirtualObject(
-        generate_layout_params_.id, layout_params, GetCurrentView(), attrs_));
-    view_stack_.push_back({std::move(view), std::move(layout_params)});
-  } else {
-    view_stack_.push_back({std::move(view), {}});
-  }
-}
-
-void DexViewBuilder::FinishView() {
-  if (view_stack_.size() == 1) {
-    method_->BuildReturn(GetCurrentView(), /*is_object=*/true);
-  } else {
-    // parent.add(view, layout_params)
-    method_->AddInstruction(Instruction::InvokeVirtual(
-        add_view_.id, /*dest=*/{}, GetParentView(), GetCurrentView(), GetCurrentLayoutParams()));
-    // xml.next(); // end tag
-    method_->AddInstruction(Instruction::InvokeInterface(xml_next_.id, {}, xml_));
-  }
-  PopViewStack();
-}
-
-LiveRegister DexViewBuilder::AcquireRegister() { return method_->AllocRegister(); }
-
-Value DexViewBuilder::GetCurrentView() const { return view_stack_.back().view; }
-Value DexViewBuilder::GetCurrentLayoutParams() const {
-  return view_stack_.back().layout_params.value();
-}
-Value DexViewBuilder::GetParentView() const { return view_stack_[view_stack_.size() - 2].view; }
-
-void DexViewBuilder::PopViewStack() {
-  // Unconditionally release the view register.
-  view_stack_.pop_back();
-}
-
-}  // namespace startop
diff --git a/startop/view_compiler/dex_layout_compiler.h b/startop/view_compiler/dex_layout_compiler.h
deleted file mode 100644
index a34ed1f..0000000
--- a/startop/view_compiler/dex_layout_compiler.h
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2018 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.
- */
-
-#ifndef DEX_LAYOUT_COMPILER_H_
-#define DEX_LAYOUT_COMPILER_H_
-
-#include "dex_builder.h"
-
-#include <codecvt>
-#include <locale>
-#include <string>
-#include <vector>
-
-namespace startop {
-
-// This visitor does the actual view compilation, using a supplied builder.
-template <typename Builder>
-class LayoutCompilerVisitor {
- public:
-  explicit LayoutCompilerVisitor(Builder* builder) : builder_{builder} {}
-
-  void VisitStartDocument() { builder_->Start(); }
-  void VisitEndDocument() { builder_->Finish(); }
-  void VisitStartTag(const std::u16string& name) {
-    parent_stack_.push_back(ViewEntry{
-        std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t>{}.to_bytes(name), {}});
-  }
-  void VisitEndTag() {
-    auto entry = parent_stack_.back();
-    parent_stack_.pop_back();
-
-    if (parent_stack_.empty()) {
-      GenerateCode(entry);
-    } else {
-      parent_stack_.back().children.push_back(entry);
-    }
-  }
-
- private:
-  struct ViewEntry {
-    std::string name;
-    std::vector<ViewEntry> children;
-  };
-
-  void GenerateCode(const ViewEntry& view) {
-    builder_->StartView(view.name, !view.children.empty());
-    for (const auto& child : view.children) {
-      GenerateCode(child);
-    }
-    builder_->FinishView();
-  }
-
-  Builder* builder_;
-
-  std::vector<ViewEntry> parent_stack_;
-};
-
-class DexViewBuilder {
- public:
-  DexViewBuilder(dex::MethodBuilder* method);
-
-  void Start();
-  void Finish();
-  void StartView(const std::string& name, bool is_viewgroup);
-  void FinishView();
-
- private:
-  // Accessors for the stack of views that are under construction.
-  dex::LiveRegister AcquireRegister();
-  dex::Value GetCurrentView() const;
-  dex::Value GetCurrentLayoutParams() const;
-  dex::Value GetParentView() const;
-  void PopViewStack();
-
-  // Methods to simplify building different code fragments.
-  void BuildGetLayoutInflater(dex::Value dest);
-  void BuildGetResources(dex::Value dest);
-  void BuildGetLayoutResource(dex::Value dest, dex::Value resources, dex::Value resid);
-  void BuildLayoutResourceToAttributeSet(dex::Value dest, dex::Value layout_resource);
-  void BuildXmlNext();
-  void BuildTryCreateView(dex::Value dest, dex::Value parent, dex::Value classname);
-
-  dex::MethodBuilder* method_;
-
-  // Parameters to the generated method
-  dex::Value const context_;
-  dex::Value const resid_;
-
-  // Registers used for code generation
-  const dex::LiveRegister inflater_;
-  const dex::LiveRegister xml_;
-  const dex::LiveRegister attrs_;
-  const dex::LiveRegister classname_tmp_;
-
-  const dex::MethodDeclData xml_next_;
-  const dex::MethodDeclData try_create_view_;
-  const dex::MethodDeclData generate_layout_params_;
-  const dex::MethodDeclData add_view_;
-
-  // Keep track of the views currently in progress.
-  struct ViewEntry {
-    dex::LiveRegister view;
-    std::optional<dex::LiveRegister> layout_params;
-  };
-  std::vector<ViewEntry> view_stack_;
-};
-
-}  // namespace startop
-
-#endif  // DEX_LAYOUT_COMPILER_H_
diff --git a/startop/view_compiler/dex_testcase_generator.cc b/startop/view_compiler/dex_testcase_generator.cc
deleted file mode 100644
index 5dda59e..0000000
--- a/startop/view_compiler/dex_testcase_generator.cc
+++ /dev/null
@@ -1,353 +0,0 @@
-/*
- * Copyright (C) 2018 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.
- */
-
-#include "android-base/logging.h"
-#include "dex_builder.h"
-
-#include <fstream>
-#include <string>
-
-// Adding tests here requires changes in several other places. See README.md in
-// the view_compiler directory for more information.
-
-using namespace startop::dex;
-using namespace std;
-
-void GenerateTrivialDexFile(const string& outdir) {
-  DexBuilder dex_file;
-
-  ClassBuilder cbuilder{dex_file.MakeClass("android.startop.test.testcases.Trivial")};
-  cbuilder.set_source_file("dex_testcase_generator.cc#GenerateTrivialDexFile");
-
-  slicer::MemView image{dex_file.CreateImage()};
-  std::ofstream out_file(outdir + "/trivial.dex");
-  out_file.write(image.ptr<const char>(), image.size());
-}
-
-// Generates test cases that test around 1 instruction.
-void GenerateSimpleTestCases(const string& outdir) {
-  DexBuilder dex_file;
-
-  ClassBuilder cbuilder{dex_file.MakeClass("android.startop.test.testcases.SimpleTests")};
-  cbuilder.set_source_file("dex_testcase_generator.cc#GenerateSimpleTestCases");
-
-  // int return5() { return 5; }
-  auto return5{cbuilder.CreateMethod("return5", Prototype{TypeDescriptor::Int()})};
-  {
-    LiveRegister r{return5.AllocRegister()};
-    return5.BuildConst4(r, 5);
-    return5.BuildReturn(r);
-  }
-  return5.Encode();
-
-  // int return5() { return 5; }
-  auto integer_type{TypeDescriptor::FromClassname("java.lang.Integer")};
-  auto returnInteger5{cbuilder.CreateMethod("returnInteger5", Prototype{integer_type})};
-  [&](MethodBuilder& method) {
-    LiveRegister five{method.AllocRegister()};
-    method.BuildConst4(five, 5);
-    LiveRegister object{method.AllocRegister()};
-    method.BuildNew(
-        object, integer_type, Prototype{TypeDescriptor::Void(), TypeDescriptor::Int()}, five);
-    method.BuildReturn(object, /*is_object=*/true);
-  }(returnInteger5);
-  returnInteger5.Encode();
-
-  // // int returnParam(int x) { return x; }
-  auto returnParam{cbuilder.CreateMethod("returnParam",
-                                         Prototype{TypeDescriptor::Int(), TypeDescriptor::Int()})};
-  returnParam.BuildReturn(Value::Parameter(0));
-  returnParam.Encode();
-
-  // int returnStringLength(String x) { return x.length(); }
-  auto string_type{TypeDescriptor::FromClassname("java.lang.String")};
-  MethodDeclData string_length{
-      dex_file.GetOrDeclareMethod(string_type, "length", Prototype{TypeDescriptor::Int()})};
-
-  auto returnStringLength{
-      cbuilder.CreateMethod("returnStringLength", Prototype{TypeDescriptor::Int(), string_type})};
-  {
-    LiveRegister result = returnStringLength.AllocRegister();
-    returnStringLength.AddInstruction(
-        Instruction::InvokeVirtual(string_length.id, result, Value::Parameter(0)));
-    returnStringLength.BuildReturn(result);
-  }
-  returnStringLength.Encode();
-
-  // int returnIfZero(int x) { if (x == 0) { return 5; } else { return 3; } }
-  MethodBuilder returnIfZero{cbuilder.CreateMethod(
-      "returnIfZero", Prototype{TypeDescriptor::Int(), TypeDescriptor::Int()})};
-  {
-    LiveRegister resultIfZero{returnIfZero.AllocRegister()};
-    Value else_target{returnIfZero.MakeLabel()};
-    returnIfZero.AddInstruction(Instruction::OpWithArgs(
-        Instruction::Op::kBranchEqz, /*dest=*/{}, Value::Parameter(0), else_target));
-    // else branch
-    returnIfZero.BuildConst4(resultIfZero, 3);
-    returnIfZero.AddInstruction(
-        Instruction::OpWithArgs(Instruction::Op::kReturn, /*dest=*/{}, resultIfZero));
-    // then branch
-    returnIfZero.AddInstruction(
-        Instruction::OpWithArgs(Instruction::Op::kBindLabel, /*dest=*/{}, else_target));
-    returnIfZero.BuildConst4(resultIfZero, 5);
-    returnIfZero.AddInstruction(
-        Instruction::OpWithArgs(Instruction::Op::kReturn, /*dest=*/{}, resultIfZero));
-  }
-  returnIfZero.Encode();
-
-  // int returnIfNotZero(int x) { if (x != 0) { return 5; } else { return 3; } }
-  MethodBuilder returnIfNotZero{cbuilder.CreateMethod(
-      "returnIfNotZero", Prototype{TypeDescriptor::Int(), TypeDescriptor::Int()})};
-  {
-    LiveRegister resultIfNotZero{returnIfNotZero.AllocRegister()};
-    Value else_target{returnIfNotZero.MakeLabel()};
-    returnIfNotZero.AddInstruction(Instruction::OpWithArgs(
-        Instruction::Op::kBranchNEqz, /*dest=*/{}, Value::Parameter(0), else_target));
-    // else branch
-    returnIfNotZero.BuildConst4(resultIfNotZero, 3);
-    returnIfNotZero.AddInstruction(
-        Instruction::OpWithArgs(Instruction::Op::kReturn, /*dest=*/{}, resultIfNotZero));
-    // then branch
-    returnIfNotZero.AddInstruction(
-        Instruction::OpWithArgs(Instruction::Op::kBindLabel, /*dest=*/{}, else_target));
-    returnIfNotZero.BuildConst4(resultIfNotZero, 5);
-    returnIfNotZero.AddInstruction(
-        Instruction::OpWithArgs(Instruction::Op::kReturn, /*dest=*/{}, resultIfNotZero));
-  }
-  returnIfNotZero.Encode();
-
-  // Make sure backwards branches work too.
-  //
-  // Pseudo code for test:
-  // {
-  //   zero = 0;
-  //   result = 1;
-  //   if (zero == 0) goto B;
-  // A:
-  //   return result;
-  // B:
-  //   result = 2;
-  //   if (zero == 0) goto A;
-  //   result = 3;
-  //   return result;
-  // }
-  // If it runs correctly, this test should return 2.
-  MethodBuilder backwardsBranch{
-      cbuilder.CreateMethod("backwardsBranch", Prototype{TypeDescriptor::Int()})};
-  [](MethodBuilder& method) {
-    LiveRegister zero = method.AllocRegister();
-    LiveRegister result = method.AllocRegister();
-    Value labelA = method.MakeLabel();
-    Value labelB = method.MakeLabel();
-    method.BuildConst4(zero, 0);
-    method.BuildConst4(result, 1);
-    method.AddInstruction(
-        Instruction::OpWithArgs(Instruction::Op::kBranchEqz, /*dest=*/{}, zero, labelB));
-
-    method.AddInstruction(
-        Instruction::OpWithArgs(Instruction::Op::kBindLabel, /*dest=*/{}, labelA));
-    method.BuildReturn(result);
-
-    method.AddInstruction(
-        Instruction::OpWithArgs(Instruction::Op::kBindLabel, /*dest=*/{}, labelB));
-    method.BuildConst4(result, 2);
-    method.AddInstruction(
-        Instruction::OpWithArgs(Instruction::Op::kBranchEqz, /*dest=*/{}, zero, labelA));
-
-    method.BuildConst4(result, 3);
-    method.BuildReturn(result);
-  }(backwardsBranch);
-  backwardsBranch.Encode();
-
-  // Test that we can make a null value. Basically:
-  //
-  // public static String returnNull() { return null; }
-  MethodBuilder returnNull{cbuilder.CreateMethod("returnNull", Prototype{string_type})};
-  [](MethodBuilder& method) {
-    LiveRegister zero = method.AllocRegister();
-    method.BuildConst4(zero, 0);
-    method.BuildReturn(zero, /*is_object=*/true);
-  }(returnNull);
-  returnNull.Encode();
-
-  // Test that we can make String literals. Basically:
-  //
-  // public static String makeString() { return "Hello, World!"; }
-  MethodBuilder makeString{cbuilder.CreateMethod("makeString", Prototype{string_type})};
-  [](MethodBuilder& method) {
-    LiveRegister string = method.AllocRegister();
-    method.BuildConstString(string, "Hello, World!");
-    method.BuildReturn(string, /*is_object=*/true);
-  }(makeString);
-  makeString.Encode();
-
-  // Make sure strings are sorted correctly.
-  //
-  // int returnStringIfZeroAB(int x) { if (x == 0) { return "a"; } else { return "b"; } }
-  MethodBuilder returnStringIfZeroAB{
-      cbuilder.CreateMethod("returnStringIfZeroAB", Prototype{string_type, TypeDescriptor::Int()})};
-  [&](MethodBuilder& method) {
-    LiveRegister resultIfZero{method.AllocRegister()};
-    Value else_target{method.MakeLabel()};
-    method.AddInstruction(Instruction::OpWithArgs(
-        Instruction::Op::kBranchEqz, /*dest=*/{}, Value::Parameter(0), else_target));
-    // else branch
-    method.BuildConstString(resultIfZero, "b");
-    method.AddInstruction(
-        Instruction::OpWithArgs(Instruction::Op::kReturnObject, /*dest=*/{}, resultIfZero));
-    // then branch
-    method.AddInstruction(
-        Instruction::OpWithArgs(Instruction::Op::kBindLabel, /*dest=*/{}, else_target));
-    method.BuildConstString(resultIfZero, "a");
-    method.AddInstruction(
-        Instruction::OpWithArgs(Instruction::Op::kReturnObject, /*dest=*/{}, resultIfZero));
-    method.Encode();
-  }(returnStringIfZeroAB);
-  // int returnStringIfZeroAB(int x) { if (x == 0) { return "b"; } else { return "a"; } }
-  MethodBuilder returnStringIfZeroBA{
-      cbuilder.CreateMethod("returnStringIfZeroBA", Prototype{string_type, TypeDescriptor::Int()})};
-  [&](MethodBuilder& method) {
-    LiveRegister resultIfZero{method.AllocRegister()};
-    Value else_target{method.MakeLabel()};
-    method.AddInstruction(Instruction::OpWithArgs(
-        Instruction::Op::kBranchEqz, /*dest=*/{}, Value::Parameter(0), else_target));
-    // else branch
-    method.BuildConstString(resultIfZero, "a");
-    method.AddInstruction(
-        Instruction::OpWithArgs(Instruction::Op::kReturnObject, /*dest=*/{}, resultIfZero));
-    // then branch
-    method.AddInstruction(
-        Instruction::OpWithArgs(Instruction::Op::kBindLabel, /*dest=*/{}, else_target));
-    method.BuildConstString(resultIfZero, "b");
-    method.AddInstruction(
-        Instruction::OpWithArgs(Instruction::Op::kReturnObject, /*dest=*/{}, resultIfZero));
-    method.Encode();
-  }(returnStringIfZeroBA);
-
-  // Make sure we can invoke static methods that return an object
-  // String invokeStaticReturnObject(int n, int radix) { return java.lang.Integer.toString(n,
-  // radix); }
-  MethodBuilder invokeStaticReturnObject{
-      cbuilder.CreateMethod("invokeStaticReturnObject",
-                            Prototype{string_type, TypeDescriptor::Int(), TypeDescriptor::Int()})};
-  [&](MethodBuilder& method) {
-    LiveRegister result{method.AllocRegister()};
-    MethodDeclData to_string{dex_file.GetOrDeclareMethod(
-        TypeDescriptor::FromClassname("java.lang.Integer"),
-        "toString",
-        Prototype{string_type, TypeDescriptor::Int(), TypeDescriptor::Int()})};
-    method.AddInstruction(Instruction::InvokeStaticObject(
-        to_string.id, result, Value::Parameter(0), Value::Parameter(1)));
-    method.BuildReturn(result, /*is_object=*/true);
-    method.Encode();
-  }(invokeStaticReturnObject);
-
-  // Make sure we can invoke virtual methods that return an object
-  // String invokeVirtualReturnObject(String s, int n) { return s.substring(n); }
-  MethodBuilder invokeVirtualReturnObject{cbuilder.CreateMethod(
-      "invokeVirtualReturnObject", Prototype{string_type, string_type, TypeDescriptor::Int()})};
-  [&](MethodBuilder& method) {
-    LiveRegister result{method.AllocRegister()};
-    MethodDeclData substring{dex_file.GetOrDeclareMethod(
-        string_type, "substring", Prototype{string_type, TypeDescriptor::Int()})};
-    method.AddInstruction(Instruction::InvokeVirtualObject(
-        substring.id, result, Value::Parameter(0), Value::Parameter(1)));
-    method.BuildReturn(result, /*is_object=*/true);
-    method.Encode();
-  }(invokeVirtualReturnObject);
-
-  // Make sure we can cast objects
-  // String castObjectToString(Object o) { return (String)o; }
-  MethodBuilder castObjectToString{cbuilder.CreateMethod(
-      "castObjectToString",
-      Prototype{string_type, TypeDescriptor::FromClassname("java.lang.Object")})};
-  [&](MethodBuilder& method) {
-    const ir::Type* type_def = dex_file.GetOrAddType(string_type.descriptor());
-    method.AddInstruction(
-        Instruction::Cast(Value::Parameter(0), Value::Type(type_def->orig_index)));
-    method.BuildReturn(Value::Parameter(0), /*is_object=*/true);
-    method.Encode();
-  }(castObjectToString);
-
-  TypeDescriptor test_class = TypeDescriptor::FromClassname("android.startop.test.TestClass");
-
-  // Read a static field
-  // int readStaticField() { return TestClass.staticInteger; }
-  MethodBuilder readStaticField{
-      cbuilder.CreateMethod("readStaticField", Prototype{TypeDescriptor::Int()})};
-  [&](MethodBuilder& method) {
-    const ir::FieldDecl* field =
-        dex_file.GetOrAddField(test_class, "staticInteger", TypeDescriptor::Int());
-    LiveRegister result{method.AllocRegister()};
-    method.AddInstruction(Instruction::GetStaticField(field->orig_index, result));
-    method.BuildReturn(result, /*is_object=*/false);
-    method.Encode();
-  }(readStaticField);
-
-  // Set a static field
-  // void setStaticField() { TestClass.staticInteger = 7; }
-  MethodBuilder setStaticField{
-      cbuilder.CreateMethod("setStaticField", Prototype{TypeDescriptor::Void()})};
-  [&](MethodBuilder& method) {
-    const ir::FieldDecl* field =
-        dex_file.GetOrAddField(test_class, "staticInteger", TypeDescriptor::Int());
-    LiveRegister number{method.AllocRegister()};
-    method.BuildConst4(number, 7);
-    method.AddInstruction(Instruction::SetStaticField(field->orig_index, number));
-    method.BuildReturn();
-    method.Encode();
-  }(setStaticField);
-
-  // Read an instance field
-  // int readInstanceField(TestClass obj) { return obj.instanceField; }
-  MethodBuilder readInstanceField{
-      cbuilder.CreateMethod("readInstanceField", Prototype{TypeDescriptor::Int(), test_class})};
-  [&](MethodBuilder& method) {
-    const ir::FieldDecl* field =
-        dex_file.GetOrAddField(test_class, "instanceField", TypeDescriptor::Int());
-    LiveRegister result{method.AllocRegister()};
-    method.AddInstruction(Instruction::GetField(field->orig_index, result, Value::Parameter(0)));
-    method.BuildReturn(result, /*is_object=*/false);
-    method.Encode();
-  }(readInstanceField);
-
-  // Set an instance field
-  // void setInstanceField(TestClass obj) { obj.instanceField = 7; }
-  MethodBuilder setInstanceField{
-      cbuilder.CreateMethod("setInstanceField", Prototype{TypeDescriptor::Void(), test_class})};
-  [&](MethodBuilder& method) {
-    const ir::FieldDecl* field =
-        dex_file.GetOrAddField(test_class, "instanceField", TypeDescriptor::Int());
-    LiveRegister number{method.AllocRegister()};
-    method.BuildConst4(number, 7);
-    method.AddInstruction(Instruction::SetField(field->orig_index, Value::Parameter(0), number));
-    method.BuildReturn();
-    method.Encode();
-  }(setInstanceField);
-
-  slicer::MemView image{dex_file.CreateImage()};
-  std::ofstream out_file(outdir + "/simple.dex");
-  out_file.write(image.ptr<const char>(), image.size());
-}
-
-int main(int argc, char** argv) {
-  CHECK_EQ(argc, 2);
-
-  string outdir = argv[1];
-
-  GenerateTrivialDexFile(outdir);
-  GenerateSimpleTestCases(outdir);
-}
diff --git a/startop/view_compiler/java_lang_builder.cc b/startop/view_compiler/java_lang_builder.cc
deleted file mode 100644
index 920caee..0000000
--- a/startop/view_compiler/java_lang_builder.cc
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright (C) 2018 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.
- */
-
-#include "java_lang_builder.h"
-
-#include "android-base/stringprintf.h"
-
-using android::base::StringPrintf;
-using std::string;
-
-void JavaLangViewBuilder::Start() const {
-  out_ << StringPrintf("package %s;\n", package_.c_str())
-       << "import android.content.Context;\n"
-          "import android.content.res.Resources;\n"
-          "import android.content.res.XmlResourceParser;\n"
-          "import android.util.AttributeSet;\n"
-          "import android.util.Xml;\n"
-          "import android.view.*;\n"
-          "import android.widget.*;\n"
-          "\n"
-          "public final class CompiledView {\n"
-          "\n"
-          "static <T extends View> T createView(Context context, AttributeSet attrs, View parent, "
-          "String name, LayoutInflater.Factory factory, LayoutInflater.Factory2 factory2) {"
-          "\n"
-          "  if (factory2 != null) {\n"
-          "    return (T)factory2.onCreateView(parent, name, context, attrs);\n"
-          "  } else if (factory != null) {\n"
-          "    return (T)factory.onCreateView(name, context, attrs);\n"
-          "  }\n"
-          // TODO: find a way to call the private factory
-          "  return null;\n"
-          "}\n"
-          "\n"
-          "  public static View inflate(Context context) {\n"
-          "    try {\n"
-          "      LayoutInflater inflater = LayoutInflater.from(context);\n"
-          "      LayoutInflater.Factory factory = inflater.getFactory();\n"
-          "      LayoutInflater.Factory2 factory2 = inflater.getFactory2();\n"
-          "      Resources res = context.getResources();\n"
-       << StringPrintf("      XmlResourceParser xml = res.getLayout(%s.R.layout.%s);\n",
-                       package_.c_str(),
-                       layout_name_.c_str())
-       << "      AttributeSet attrs = Xml.asAttributeSet(xml);\n"
-          // The Java-language XmlPullParser needs a call to next to find the start document tag.
-          "      xml.next(); // start document\n";
-}
-
-void JavaLangViewBuilder::Finish() const {
-  out_ << "    } catch (Exception e) {\n"
-          "      return null;\n"
-          "    }\n"  // end try
-          "  }\n"    // end inflate
-          "}\n";     // end CompiledView
-}
-
-void JavaLangViewBuilder::StartView(const string& class_name, bool /*is_viewgroup*/) {
-  const string view_var = MakeVar("view");
-  const string layout_var = MakeVar("layout");
-  std::string parent = "null";
-  if (!view_stack_.empty()) {
-    const StackEntry& parent_entry = view_stack_.back();
-    parent = parent_entry.view_var;
-  }
-  out_ << "      xml.next(); // <" << class_name << ">\n"
-       << StringPrintf("      %s %s = createView(context, attrs, %s, \"%s\", factory, factory2);\n",
-                       class_name.c_str(),
-                       view_var.c_str(),
-                       parent.c_str(),
-                       class_name.c_str())
-       << StringPrintf("      if (%s == null) %s = new %s(context, attrs);\n",
-                       view_var.c_str(),
-                       view_var.c_str(),
-                       class_name.c_str());
-  if (!view_stack_.empty()) {
-    out_ << StringPrintf("      ViewGroup.LayoutParams %s = %s.generateLayoutParams(attrs);\n",
-                         layout_var.c_str(),
-                         parent.c_str());
-  }
-  view_stack_.push_back({class_name, view_var, layout_var});
-}
-
-void JavaLangViewBuilder::FinishView() {
-  const StackEntry var = view_stack_.back();
-  view_stack_.pop_back();
-  if (!view_stack_.empty()) {
-    const string& parent = view_stack_.back().view_var;
-    out_ << StringPrintf("      xml.next(); // </%s>\n", var.class_name.c_str())
-         << StringPrintf("      %s.addView(%s, %s);\n",
-                         parent.c_str(),
-                         var.view_var.c_str(),
-                         var.layout_params_var.c_str());
-  } else {
-    out_ << StringPrintf("      return %s;\n", var.view_var.c_str());
-  }
-}
-
-const std::string JavaLangViewBuilder::MakeVar(std::string prefix) {
-  std::stringstream v;
-  v << prefix << view_id_++;
-  return v.str();
-}
diff --git a/startop/view_compiler/java_lang_builder.h b/startop/view_compiler/java_lang_builder.h
deleted file mode 100644
index 69356d3..0000000
--- a/startop/view_compiler/java_lang_builder.h
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2018 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.
- */
-#ifndef JAVA_LANG_BUILDER_H_
-#define JAVA_LANG_BUILDER_H_
-
-#include <iostream>
-#include <sstream>
-#include <vector>
-
-// Build Java language code to instantiate views.
-//
-// This has a very small interface to make it easier to generate additional
-// backends, such as a direct-to-DEX version.
-class JavaLangViewBuilder {
- public:
-  JavaLangViewBuilder(std::string package, std::string layout_name, std::ostream& out = std::cout)
-      : package_(package), layout_name_(layout_name), out_(out) {}
-
-  // Begin generating a class. Adds the package boilerplate, etc.
-  void Start() const;
-  // Finish generating a class, closing off any open curly braces, etc.
-  void Finish() const;
-
-  // Begin creating a view (i.e. process the opening tag)
-  void StartView(const std::string& class_name, bool is_viewgroup);
-  // Finish a view, after all of its child nodes have been processed.
-  void FinishView();
-
- private:
-  const std::string MakeVar(std::string prefix);
-
-  std::string const package_;
-  std::string const layout_name_;
-
-  std::ostream& out_;
-
-  size_t view_id_ = 0;
-
-  struct StackEntry {
-      // The class name for this view object
-      const std::string class_name;
-
-      // The variable name that is holding the view object
-      const std::string view_var;
-
-      // The variable name that holds the object's layout parameters
-      const std::string layout_params_var;
-  };
-  std::vector<StackEntry> view_stack_;
-};
-
-#endif  // JAVA_LANG_BUILDER_H_
diff --git a/startop/view_compiler/layout_validation.cc b/startop/view_compiler/layout_validation.cc
deleted file mode 100644
index 8c77377..0000000
--- a/startop/view_compiler/layout_validation.cc
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2018 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.
- */
-
-#include "layout_validation.h"
-
-#include "android-base/stringprintf.h"
-
-namespace startop {
-
-void LayoutValidationVisitor::VisitStartTag(const std::u16string& name) {
-  if (0 == name.compare(u"merge")) {
-    message_ = "Merge tags are not supported";
-    can_compile_ = false;
-  }
-  if (0 == name.compare(u"include")) {
-    message_ = "Include tags are not supported";
-    can_compile_ = false;
-  }
-  if (0 == name.compare(u"view")) {
-    message_ = "View tags are not supported";
-    can_compile_ = false;
-  }
-  if (0 == name.compare(u"fragment")) {
-    message_ = "Fragment tags are not supported";
-    can_compile_ = false;
-  }
-}
-
-}  // namespace startop
\ No newline at end of file
diff --git a/startop/view_compiler/layout_validation.h b/startop/view_compiler/layout_validation.h
deleted file mode 100644
index bed34bb..0000000
--- a/startop/view_compiler/layout_validation.h
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2018 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.
- */
-
-#ifndef LAYOUT_VALIDATION_H_
-#define LAYOUT_VALIDATION_H_
-
-#include "dex_builder.h"
-
-#include <string>
-
-namespace startop {
-
-// This visitor determines whether a layout can be compiled. Since we do not currently support all
-// features, such as includes and merges, we need to pre-validate the layout before we start
-// compiling.
-class LayoutValidationVisitor {
- public:
-  void VisitStartDocument() const {}
-  void VisitEndDocument() const {}
-  void VisitStartTag(const std::u16string& name);
-  void VisitEndTag() const {}
-
-  const std::string& message() const { return message_; }
-  bool can_compile() const { return can_compile_; }
-
- private:
-  std::string message_{"Okay"};
-  bool can_compile_{true};
-};
-
-}  // namespace startop
-
-#endif  // LAYOUT_VALIDATION_H_
diff --git a/startop/view_compiler/layout_validation_test.cc b/startop/view_compiler/layout_validation_test.cc
deleted file mode 100644
index b74cdae..0000000
--- a/startop/view_compiler/layout_validation_test.cc
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- * Copyright (C) 2018 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.
- */
-#include "tinyxml_layout_parser.h"
-
-#include "gtest/gtest.h"
-
-using startop::CanCompileLayout;
-using std::string;
-
-namespace {
-void ValidateXmlText(const string& xml, bool expected) {
-  tinyxml2::XMLDocument doc;
-  doc.Parse(xml.c_str());
-  EXPECT_EQ(CanCompileLayout(doc), expected);
-}
-}  // namespace
-
-TEST(LayoutValidationTest, SingleButtonLayout) {
-  const string xml = R"(<?xml version="1.0" encoding="utf-8"?>
-<Button xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:text="Hello, World!">
-
-</Button>)";
-  ValidateXmlText(xml, /*expected=*/true);
-}
-
-TEST(LayoutValidationTest, SmallConstraintLayout) {
-  const string xml = R"(<?xml version="1.0" encoding="utf-8"?>
-<android.support.constraint.ConstraintLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-
-    <Button
-        android:id="@+id/button6"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginEnd="16dp"
-        android:layout_marginBottom="16dp"
-        android:text="Button"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toEndOf="parent" />
-
-    <Button
-        android:id="@+id/button7"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginEnd="8dp"
-        android:layout_marginBottom="16dp"
-        android:text="Button2"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toStartOf="@+id/button6" />
-
-    <Button
-        android:id="@+id/button8"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginEnd="8dp"
-        android:layout_marginBottom="16dp"
-        android:text="Button1"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toStartOf="@+id/button7" />
-</android.support.constraint.ConstraintLayout>)";
-  ValidateXmlText(xml, /*expected=*/true);
-}
-
-TEST(LayoutValidationTest, MergeNode) {
-  const string xml = R"(<?xml version="1.0" encoding="utf-8"?>
-<merge xmlns:android="http://schemas.android.com/apk/res/android">
-
-    <TextView
-        android:id="@+id/textView3"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:text="TextView" />
-
-    <Button
-        android:id="@+id/button9"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:text="Button" />
-</merge>)";
-  ValidateXmlText(xml, /*expected=*/false);
-}
-
-TEST(LayoutValidationTest, IncludeLayout) {
-  const string xml = R"(<?xml version="1.0" encoding="utf-8"?>
-<android.support.constraint.ConstraintLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-
-    <include
-        layout="@layout/single_button_layout"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent" />
-</android.support.constraint.ConstraintLayout>)";
-  ValidateXmlText(xml, /*expected=*/false);
-}
-
-TEST(LayoutValidationTest, ViewNode) {
-  const string xml = R"(<?xml version="1.0" encoding="utf-8"?>
-<android.support.constraint.ConstraintLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-
-    <view
-        class="android.support.design.button.MaterialButton"
-        id="@+id/view"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent" />
-</android.support.constraint.ConstraintLayout>)";
-  ValidateXmlText(xml, /*expected=*/false);
-}
-
-TEST(LayoutValidationTest, FragmentNode) {
-  // This test case is from https://developer.android.com/guide/components/fragments
-  const string xml = R"(<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:orientation="horizontal"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-    <fragment android:name="com.example.news.ArticleListFragment"
-            android:id="@+id/list"
-            android:layout_weight="1"
-            android:layout_width="0dp"
-            android:layout_height="match_parent" />
-    <fragment android:name="com.example.news.ArticleReaderFragment"
-            android:id="@+id/viewer"
-            android:layout_weight="2"
-            android:layout_width="0dp"
-            android:layout_height="match_parent" />
-</LinearLayout>)";
-  ValidateXmlText(xml, /*expected=*/false);
-}
diff --git a/startop/view_compiler/main.cc b/startop/view_compiler/main.cc
deleted file mode 100644
index 11ecde2..0000000
--- a/startop/view_compiler/main.cc
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * Copyright (C) 2018 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.
- */
-
-#include "gflags/gflags.h"
-
-#include "android-base/stringprintf.h"
-#include "apk_layout_compiler.h"
-#include "dex_builder.h"
-#include "dex_layout_compiler.h"
-#include "java_lang_builder.h"
-#include "layout_validation.h"
-#include "tinyxml_layout_parser.h"
-#include "util.h"
-
-#include "tinyxml2.h"
-
-#include <fstream>
-#include <iostream>
-#include <sstream>
-#include <string>
-#include <vector>
-
-namespace {
-
-using namespace tinyxml2;
-using android::base::StringPrintf;
-using startop::dex::ClassBuilder;
-using startop::dex::DexBuilder;
-using startop::dex::MethodBuilder;
-using startop::dex::Prototype;
-using startop::dex::TypeDescriptor;
-using namespace startop::util;
-using std::string;
-
-constexpr char kStdoutFilename[]{"stdout"};
-
-DEFINE_bool(apk, false, "Compile layouts in an APK");
-DEFINE_bool(dex, false, "Generate a DEX file instead of Java");
-DEFINE_int32(infd, -1, "Read input from the given file descriptor");
-DEFINE_string(out, kStdoutFilename, "Where to write the generated class");
-DEFINE_string(package, "", "The package name for the generated class (required)");
-
-template <typename Visitor>
-class XmlVisitorAdapter : public XMLVisitor {
- public:
-  explicit XmlVisitorAdapter(Visitor* visitor) : visitor_{visitor} {}
-
-  bool VisitEnter(const XMLDocument& /*doc*/) override {
-    visitor_->VisitStartDocument();
-    return true;
-  }
-
-  bool VisitExit(const XMLDocument& /*doc*/) override {
-    visitor_->VisitEndDocument();
-    return true;
-  }
-
-  bool VisitEnter(const XMLElement& element, const XMLAttribute* /*firstAttribute*/) override {
-    visitor_->VisitStartTag(
-        std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t>{}.from_bytes(
-            element.Name()));
-    return true;
-  }
-
-  bool VisitExit(const XMLElement& /*element*/) override {
-    visitor_->VisitEndTag();
-    return true;
-  }
-
- private:
-  Visitor* visitor_;
-};
-
-template <typename Builder>
-void CompileLayout(XMLDocument* xml, Builder* builder) {
-  startop::LayoutCompilerVisitor visitor{builder};
-  XmlVisitorAdapter<decltype(visitor)> adapter{&visitor};
-  xml->Accept(&adapter);
-}
-
-}  // end namespace
-
-int main(int argc, char** argv) {
-  constexpr size_t kProgramName = 0;
-  constexpr size_t kFileNameParam = 1;
-  constexpr size_t kNumRequiredArgs = 1;
-
-  gflags::SetUsageMessage(
-      "Compile XML layout files into equivalent Java language code\n"
-      "\n"
-      "  example usage:  viewcompiler layout.xml --package com.example.androidapp");
-  gflags::ParseCommandLineFlags(&argc, &argv, /*remove_flags*/ true);
-
-  gflags::CommandLineFlagInfo cmd = gflags::GetCommandLineFlagInfoOrDie("package");
-  if (argc < kNumRequiredArgs || cmd.is_default) {
-    gflags::ShowUsageWithFlags(argv[kProgramName]);
-    return 1;
-  }
-
-  const bool is_stdout = FLAGS_out == kStdoutFilename;
-
-  std::ofstream outfile;
-  if (!is_stdout) {
-    outfile.open(FLAGS_out);
-  }
-
-  if (FLAGS_apk) {
-    const startop::CompilationTarget target =
-        FLAGS_dex ? startop::CompilationTarget::kDex : startop::CompilationTarget::kJavaLanguage;
-    if (FLAGS_infd >= 0) {
-      startop::CompileApkLayoutsFd(
-          android::base::unique_fd{FLAGS_infd}, target, is_stdout ? std::cout : outfile);
-    } else {
-      if (argc < 2) {
-        gflags::ShowUsageWithFlags(argv[kProgramName]);
-        return 1;
-      }
-      const char* const filename = argv[kFileNameParam];
-      startop::CompileApkLayouts(filename, target, is_stdout ? std::cout : outfile);
-    }
-    return 0;
-  }
-
-  const char* const filename = argv[kFileNameParam];
-  const string layout_name = startop::util::FindLayoutNameFromFilename(filename);
-
-  XMLDocument xml;
-  xml.LoadFile(filename);
-
-  string message{};
-  if (!startop::CanCompileLayout(xml, &message)) {
-    LOG(ERROR) << "Layout not supported: " << message;
-    return 1;
-  }
-
-  if (FLAGS_dex) {
-    DexBuilder dex_file;
-    string class_name = StringPrintf("%s.CompiledView", FLAGS_package.c_str());
-    ClassBuilder compiled_view{dex_file.MakeClass(class_name)};
-    MethodBuilder method{compiled_view.CreateMethod(
-        layout_name,
-        Prototype{TypeDescriptor::FromClassname("android.view.View"),
-                  TypeDescriptor::FromClassname("android.content.Context"),
-                  TypeDescriptor::Int()})};
-    startop::DexViewBuilder builder{&method};
-    CompileLayout(&xml, &builder);
-    method.Encode();
-
-    slicer::MemView image{dex_file.CreateImage()};
-
-    (is_stdout ? std::cout : outfile).write(image.ptr<const char>(), image.size());
-  } else {
-    // Generate Java language output.
-    JavaLangViewBuilder builder{FLAGS_package, layout_name, is_stdout ? std::cout : outfile};
-
-    CompileLayout(&xml, &builder);
-  }
-  return 0;
-}
diff --git a/startop/view_compiler/tinyxml_layout_parser.cc b/startop/view_compiler/tinyxml_layout_parser.cc
deleted file mode 100644
index 1b3a81f..0000000
--- a/startop/view_compiler/tinyxml_layout_parser.cc
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2018 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.
- */
-#include "tinyxml_layout_parser.h"
-
-#include "layout_validation.h"
-
-namespace startop {
-
-bool CanCompileLayout(const tinyxml2::XMLDocument& xml, std::string* message) {
-  LayoutValidationVisitor validator;
-  TinyXmlVisitorAdapter adapter{&validator};
-  xml.Accept(&adapter);
-
-  if (message != nullptr) {
-    *message = validator.message();
-  }
-
-  return validator.can_compile();
-}
-
-}  // namespace startop
diff --git a/startop/view_compiler/tinyxml_layout_parser.h b/startop/view_compiler/tinyxml_layout_parser.h
deleted file mode 100644
index 8f714a2..0000000
--- a/startop/view_compiler/tinyxml_layout_parser.h
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2018 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.
- */
-#ifndef TINYXML_LAYOUT_PARSER_H_
-#define TINYXML_LAYOUT_PARSER_H_
-
-#include "tinyxml2.h"
-
-#include <codecvt>
-#include <locale>
-#include <string>
-
-namespace startop {
-
-template <typename Visitor>
-class TinyXmlVisitorAdapter : public tinyxml2::XMLVisitor {
- public:
-  explicit TinyXmlVisitorAdapter(Visitor* visitor) : visitor_{visitor} {}
-
-  bool VisitEnter(const tinyxml2::XMLDocument& /*doc*/) override {
-    visitor_->VisitStartDocument();
-    return true;
-  }
-
-  bool VisitExit(const tinyxml2::XMLDocument& /*doc*/) override {
-    visitor_->VisitEndDocument();
-    return true;
-  }
-
-  bool VisitEnter(const tinyxml2::XMLElement& element,
-                  const tinyxml2::XMLAttribute* /*firstAttribute*/) override {
-    visitor_->VisitStartTag(
-        std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t>{}.from_bytes(
-            element.Name()));
-    return true;
-  }
-
-  bool VisitExit(const tinyxml2::XMLElement& /*element*/) override {
-    visitor_->VisitEndTag();
-    return true;
-  }
-
- private:
-  Visitor* visitor_;
-};
-
-// Returns whether a layout resource represented by a TinyXML document is supported by the layout
-// compiler.
-bool CanCompileLayout(const tinyxml2::XMLDocument& xml, std::string* message = nullptr);
-
-}  // namespace startop
-
-#endif  // TINYXML_LAYOUT_PARSER_H_
diff --git a/startop/view_compiler/util.cc b/startop/view_compiler/util.cc
deleted file mode 100644
index c34d7b0..0000000
--- a/startop/view_compiler/util.cc
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2018 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.
- */
-
-#include "util.h"
-
-using std::string;
-
-namespace startop {
-namespace util {
-
-// TODO: see if we can borrow this from somewhere else, like aapt2.
-string FindLayoutNameFromFilename(const string& filename) {
-  size_t start = filename.rfind('/');
-  if (start == string::npos) {
-    start = 0;
-  } else {
-    start++;  // advance past '/' character
-  }
-  size_t end = filename.find('.', start);
-
-  return filename.substr(start, end - start);
-}
-
-}  // namespace util
-}  // namespace startop
diff --git a/startop/view_compiler/util.h b/startop/view_compiler/util.h
deleted file mode 100644
index 0176175..0000000
--- a/startop/view_compiler/util.h
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2018 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.
- */
-#ifndef VIEW_COMPILER_UTIL_H_
-#define VIEW_COMPILER_UTIL_H_
-
-#include <string>
-
-namespace startop {
-namespace util {
-
-std::string FindLayoutNameFromFilename(const std::string& filename);
-
-}  // namespace util
-}  // namespace startop
-
-#endif  // VIEW_COMPILER_UTIL_H_
diff --git a/startop/view_compiler/util_test.cc b/startop/view_compiler/util_test.cc
deleted file mode 100644
index 50682a0..0000000
--- a/startop/view_compiler/util_test.cc
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2018 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.
- */
-
-#include "util.h"
-
-#include "gtest/gtest.h"
-
-using std::string;
-
-namespace startop {
-namespace util {
-
-TEST(UtilTest, FindLayoutNameFromFilename) {
-  EXPECT_EQ("bar", startop::util::FindLayoutNameFromFilename("foo/bar.xml"));
-  EXPECT_EQ("bar", startop::util::FindLayoutNameFromFilename("bar.xml"));
-  EXPECT_EQ("bar", startop::util::FindLayoutNameFromFilename("./foo/bar.xml"));
-  EXPECT_EQ("bar", startop::util::FindLayoutNameFromFilename("/foo/bar.xml"));
-}
-
-}  // namespace util
-}  // namespace startop
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 0b70b40e..042b2a3 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -8806,7 +8806,9 @@
          * {@link android.net.ipsec.ike.SaProposal#DH_GROUP_NONE},
          * {@link android.net.ipsec.ike.SaProposal#DH_GROUP_1024_BIT_MODP},
          * {@link android.net.ipsec.ike.SaProposal#DH_GROUP_1536_BIT_MODP},
-         * {@link android.net.ipsec.ike.SaProposal#DH_GROUP_2048_BIT_MODP}
+         * {@link android.net.ipsec.ike.SaProposal#DH_GROUP_2048_BIT_MODP},
+         * {@link android.net.ipsec.ike.SaProposal#DH_GROUP_3072_BIT_MODP},
+         * {@link android.net.ipsec.ike.SaProposal#DH_GROUP_4096_BIT_MODP}
          */
         public static final String KEY_DIFFIE_HELLMAN_GROUPS_INT_ARRAY =
                 KEY_PREFIX + "diffie_hellman_groups_int_array";
@@ -8874,7 +8876,8 @@
 
         /**
          * List of supported encryption algorithms for child session. Possible values are
-         * {@link android.net.ipsec.ike.SaProposal#ENCRYPTION_ALGORITHM_AES_CBC}
+         * {@link android.net.ipsec.ike.SaProposal#ENCRYPTION_ALGORITHM_AES_CBC},
+         * {@link android.net.ipsec.ike.SaProposal#ENCRYPTION_ALGORITHM_AES_CTR}
          */
         public static final String KEY_SUPPORTED_CHILD_SESSION_ENCRYPTION_ALGORITHMS_INT_ARRAY =
                 KEY_PREFIX + "supported_child_session_encryption_algorithms_int_array";
diff --git a/telephony/java/android/telephony/PhoneNumberUtils.java b/telephony/java/android/telephony/PhoneNumberUtils.java
index cee2efb..f161f31 100644
--- a/telephony/java/android/telephony/PhoneNumberUtils.java
+++ b/telephony/java/android/telephony/PhoneNumberUtils.java
@@ -16,6 +16,7 @@
 
 package android.telephony;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -43,6 +44,7 @@
 import com.android.i18n.phonenumbers.PhoneNumberUtil;
 import com.android.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat;
 import com.android.i18n.phonenumbers.Phonenumber.PhoneNumber;
+import com.android.internal.telephony.flags.Flags;
 import com.android.telephony.Rlog;
 
 import java.lang.annotation.Retention;
@@ -2957,7 +2959,8 @@
      * @param number  The phone number used for WPS call.
      * @return {@code true} if number matches WPS pattern and {@code false} otherwise.
      */
-    public static boolean isWpsCallNumber(@Nullable String number) {
+    @FlaggedApi(Flags.FLAG_ENABLE_WPS_CHECK_API_FLAG)
+    public static boolean isWpsCallNumber(@NonNull String number) {
         return (number != null) && (number.startsWith(PREFIX_WPS)
                 || number.startsWith(PREFIX_WPS_CLIR_ACTIVATE)
                 || number.startsWith(PREFIX_WPS_CLIR_DEACTIVATE));
diff --git a/telephony/java/android/telephony/ims/ImsCallSession.java b/telephony/java/android/telephony/ims/ImsCallSession.java
index ecd7039..d30078d 100755
--- a/telephony/java/android/telephony/ims/ImsCallSession.java
+++ b/telephony/java/android/telephony/ims/ImsCallSession.java
@@ -29,6 +29,7 @@
 
 import com.android.ims.internal.IImsCallSession;
 import com.android.ims.internal.IImsVideoCallProvider;
+import com.android.internal.telephony.flags.Flags;
 import com.android.internal.telephony.util.TelephonyUtils;
 
 import java.util.ArrayList;
@@ -545,6 +546,11 @@
             try {
                 iSession.setListener(mIImsCallSessionListenerProxy);
             } catch (RemoteException e) {
+                if (Flags.ignoreAlreadyTerminatedIncomingCallBeforeRegisteringListener()) {
+                    // Registering listener failed, so other operations are not allowed.
+                    Log.e(TAG, "ImsCallSession : " + e);
+                    mClosed = true;
+                }
             }
         } else {
             mClosed = true;
diff --git a/tests/notification/Android.bp b/tests/notification/Android.bp
deleted file mode 100644
index 1c1b5a2..0000000
--- a/tests/notification/Android.bp
+++ /dev/null
@@ -1,16 +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_test {
-    name: "NotificationTests",
-    // Include all test java files.
-    srcs: ["src/**/*.java"],
-    libs: ["android.test.runner.stubs"],
-    sdk_version: "21",
-}
diff --git a/tests/notification/AndroidManifest.xml b/tests/notification/AndroidManifest.xml
deleted file mode 100644
index 7cee00a7..0000000
--- a/tests/notification/AndroidManifest.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2008 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.frameworks.tests.notification"
-    >
-    <application>
-        <uses-library android:name="android.test.runner" />
-    </application>
-
-    <instrumentation
-        android:name="android.test.InstrumentationTestRunner"
-        android:targetPackage="com.android.frameworks.tests.notification"
-        android:label="Frameworks Notification Tests" />
-</manifest>
diff --git a/tests/notification/OWNERS b/tests/notification/OWNERS
deleted file mode 100644
index 396fd12..0000000
--- a/tests/notification/OWNERS
+++ /dev/null
@@ -1 +0,0 @@
-include /services/core/java/com/android/server/notification/OWNERS
diff --git a/tests/notification/res/drawable-nodpi/arubin_hed.jpeg b/tests/notification/res/drawable-nodpi/arubin_hed.jpeg
deleted file mode 100644
index c6d8ae9..0000000
--- a/tests/notification/res/drawable-nodpi/arubin_hed.jpeg
+++ /dev/null
Binary files differ
diff --git a/tests/notification/res/drawable-nodpi/bucket.png b/tests/notification/res/drawable-nodpi/bucket.png
deleted file mode 100644
index c865649..0000000
--- a/tests/notification/res/drawable-nodpi/bucket.png
+++ /dev/null
Binary files differ
diff --git a/tests/notification/res/drawable-nodpi/matias_hed.jpg b/tests/notification/res/drawable-nodpi/matias_hed.jpg
deleted file mode 100644
index 8cc3081..0000000
--- a/tests/notification/res/drawable-nodpi/matias_hed.jpg
+++ /dev/null
Binary files differ
diff --git a/tests/notification/res/drawable-nodpi/page_hed.jpg b/tests/notification/res/drawable-nodpi/page_hed.jpg
deleted file mode 100644
index ea950c8..0000000
--- a/tests/notification/res/drawable-nodpi/page_hed.jpg
+++ /dev/null
Binary files differ
diff --git a/tests/notification/res/drawable-nodpi/romainguy_hed.jpg b/tests/notification/res/drawable-nodpi/romainguy_hed.jpg
deleted file mode 100644
index 5b7643e..0000000
--- a/tests/notification/res/drawable-nodpi/romainguy_hed.jpg
+++ /dev/null
Binary files differ
diff --git a/tests/notification/res/drawable-nodpi/romainguy_rockaway.jpg b/tests/notification/res/drawable-nodpi/romainguy_rockaway.jpg
deleted file mode 100644
index 68473ba..0000000
--- a/tests/notification/res/drawable-nodpi/romainguy_rockaway.jpg
+++ /dev/null
Binary files differ
diff --git a/tests/notification/res/drawable-xhdpi/add.png b/tests/notification/res/drawable-xhdpi/add.png
deleted file mode 100644
index 7226b3d..0000000
--- a/tests/notification/res/drawable-xhdpi/add.png
+++ /dev/null
Binary files differ
diff --git a/tests/notification/res/drawable-xhdpi/ic_dial_action_call.png b/tests/notification/res/drawable-xhdpi/ic_dial_action_call.png
deleted file mode 100644
index ca20a91..0000000
--- a/tests/notification/res/drawable-xhdpi/ic_dial_action_call.png
+++ /dev/null
Binary files differ
diff --git a/tests/notification/res/drawable-xhdpi/ic_end_call.png b/tests/notification/res/drawable-xhdpi/ic_end_call.png
deleted file mode 100644
index c464a6d..0000000
--- a/tests/notification/res/drawable-xhdpi/ic_end_call.png
+++ /dev/null
Binary files differ
diff --git a/tests/notification/res/drawable-xhdpi/ic_media_next.png b/tests/notification/res/drawable-xhdpi/ic_media_next.png
deleted file mode 100644
index 4def965..0000000
--- a/tests/notification/res/drawable-xhdpi/ic_media_next.png
+++ /dev/null
Binary files differ
diff --git a/tests/notification/res/drawable-xhdpi/ic_menu_upload.png b/tests/notification/res/drawable-xhdpi/ic_menu_upload.png
deleted file mode 100644
index f1438ed..0000000
--- a/tests/notification/res/drawable-xhdpi/ic_menu_upload.png
+++ /dev/null
Binary files differ
diff --git a/tests/notification/res/drawable-xhdpi/icon.png b/tests/notification/res/drawable-xhdpi/icon.png
deleted file mode 100644
index 189e85b..0000000
--- a/tests/notification/res/drawable-xhdpi/icon.png
+++ /dev/null
Binary files differ
diff --git a/tests/notification/res/drawable-xhdpi/stat_notify_alarm.png b/tests/notification/res/drawable-xhdpi/stat_notify_alarm.png
deleted file mode 100644
index 658d04f..0000000
--- a/tests/notification/res/drawable-xhdpi/stat_notify_alarm.png
+++ /dev/null
Binary files differ
diff --git a/tests/notification/res/drawable-xhdpi/stat_notify_calendar.png b/tests/notification/res/drawable-xhdpi/stat_notify_calendar.png
deleted file mode 100644
index 5ae7782..0000000
--- a/tests/notification/res/drawable-xhdpi/stat_notify_calendar.png
+++ /dev/null
Binary files differ
diff --git a/tests/notification/res/drawable-xhdpi/stat_notify_email.png b/tests/notification/res/drawable-xhdpi/stat_notify_email.png
deleted file mode 100644
index 23c4672..0000000
--- a/tests/notification/res/drawable-xhdpi/stat_notify_email.png
+++ /dev/null
Binary files differ
diff --git a/tests/notification/res/drawable-xhdpi/stat_notify_missed_call.png b/tests/notification/res/drawable-xhdpi/stat_notify_missed_call.png
deleted file mode 100644
index 8719eff..0000000
--- a/tests/notification/res/drawable-xhdpi/stat_notify_missed_call.png
+++ /dev/null
Binary files differ
diff --git a/tests/notification/res/drawable-xhdpi/stat_notify_sms.png b/tests/notification/res/drawable-xhdpi/stat_notify_sms.png
deleted file mode 100644
index 323cb3d..0000000
--- a/tests/notification/res/drawable-xhdpi/stat_notify_sms.png
+++ /dev/null
Binary files differ
diff --git a/tests/notification/res/drawable-xhdpi/stat_notify_snooze.png b/tests/notification/res/drawable-xhdpi/stat_notify_snooze.png
deleted file mode 100644
index 26dcda35..0000000
--- a/tests/notification/res/drawable-xhdpi/stat_notify_snooze.png
+++ /dev/null
Binary files differ
diff --git a/tests/notification/res/drawable-xhdpi/stat_notify_snooze_longer.png b/tests/notification/res/drawable-xhdpi/stat_notify_snooze_longer.png
deleted file mode 100644
index b8b2f8a..0000000
--- a/tests/notification/res/drawable-xhdpi/stat_notify_snooze_longer.png
+++ /dev/null
Binary files differ
diff --git a/tests/notification/res/drawable-xhdpi/stat_notify_talk_text.png b/tests/notification/res/drawable-xhdpi/stat_notify_talk_text.png
deleted file mode 100644
index 12cae9f..0000000
--- a/tests/notification/res/drawable-xhdpi/stat_notify_talk_text.png
+++ /dev/null
Binary files differ
diff --git a/tests/notification/res/drawable-xhdpi/stat_sys_phone_call.png b/tests/notification/res/drawable-xhdpi/stat_sys_phone_call.png
deleted file mode 100644
index db42b7c..0000000
--- a/tests/notification/res/drawable-xhdpi/stat_sys_phone_call.png
+++ /dev/null
Binary files differ
diff --git a/tests/notification/res/layout/full_screen.xml b/tests/notification/res/layout/full_screen.xml
deleted file mode 100644
index 6ff7552..0000000
--- a/tests/notification/res/layout/full_screen.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-
-    <ImageView
-        android:layout_height="match_parent"
-        android:layout_width="match_parent"
-        android:src="@drawable/page_hed"
-        android:onClick="dismiss"
-        />
-</FrameLayout>
\ No newline at end of file
diff --git a/tests/notification/res/layout/main.xml b/tests/notification/res/layout/main.xml
deleted file mode 100644
index f5a740f..0000000
--- a/tests/notification/res/layout/main.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:orientation="vertical"
-    android:layout_width="fill_parent"
-    android:layout_height="fill_parent"
-    >
-    <LinearLayout android:id="@+id/linearLayout1" android:orientation="vertical" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_width="match_parent" android:layout_margin="35dp">
-        <Button android:id="@+id/button1" android:text="@string/post_button_label" android:layout_height="wrap_content" android:layout_width="match_parent" android:onClick="doPost"></Button>
-        <Button android:id="@+id/button2" android:text="@string/remove_button_label" android:layout_height="wrap_content" android:layout_width="match_parent" android:onClick="doRemove"></Button>
-    </LinearLayout>
-</FrameLayout>
diff --git a/tests/notification/res/values/dimens.xml b/tests/notification/res/values/dimens.xml
deleted file mode 100644
index 21e7bc3..0000000
--- a/tests/notification/res/values/dimens.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-** Copyright 2012, 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>
-    <!-- The width of the big icons in notifications. -->
-    <dimen name="notification_large_icon_width">64dp</dimen>
-    <!-- The width of the big icons in notifications. -->
-    <dimen name="notification_large_icon_height">64dp</dimen>
-</resources>
diff --git a/tests/notification/res/values/strings.xml b/tests/notification/res/values/strings.xml
deleted file mode 100644
index 80bf103..0000000
--- a/tests/notification/res/values/strings.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
-    <string name="hello">Hello World, NotificationShowcaseActivity!</string>
-    <string name="app_name">NotificationShowcase</string>
-    <string name="post_button_label">Post Notifications</string>
-    <string name="remove_button_label">Remove Notifications</string>
-    <string name="answered">call answered</string>
-    <string name="ignored">call ignored</string>
-    <string name="full_screen_name">Full Screen Activity</string>
-</resources>
diff --git a/tests/notification/src/com/android/frameworks/tests/notification/NotificationTests.java b/tests/notification/src/com/android/frameworks/tests/notification/NotificationTests.java
deleted file mode 100644
index 5d639f6..0000000
--- a/tests/notification/src/com/android/frameworks/tests/notification/NotificationTests.java
+++ /dev/null
@@ -1,494 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.app;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Typeface;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Parcel;
-import android.os.SystemClock;
-import android.provider.ContactsContract;
-import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.Suppress;
-import android.text.SpannableStringBuilder;
-import android.text.TextUtils;
-import android.text.style.StyleSpan;
-import android.util.Log;
-import android.view.View;
-import android.widget.Toast;
-
-import java.lang.reflect.Method;
-import java.lang.InterruptedException;
-import java.lang.NoSuchMethodError;
-import java.lang.NoSuchMethodException;
-import java.util.ArrayList;
-
-import com.android.frameworks.tests.notification.R;
-
-public class NotificationTests extends AndroidTestCase {
-    private static final String TAG = "NOTEST";
-    public static void L(String msg, Object... args) {
-        Log.v(TAG, (args == null || args.length == 0) ? msg : String.format(msg, args));
-    }
-
-    public static final String ACTION_CREATE = "create";
-    public static final int NOTIFICATION_ID = 31338;
-
-    public static final boolean SHOW_PHONE_CALL = false;
-    public static final boolean SHOW_INBOX = true;
-    public static final boolean SHOW_BIG_TEXT = true;
-    public static final boolean SHOW_BIG_PICTURE = true;
-    public static final boolean SHOW_MEDIA = true;
-    public static final boolean SHOW_STOPWATCH = false;
-    public static final boolean SHOW_SOCIAL = false;
-    public static final boolean SHOW_CALENDAR = false;
-    public static final boolean SHOW_PROGRESS = false;
-
-    private static Bitmap getBitmap(Context context, int resId) {
-        int largeIconWidth = (int) context.getResources()
-                .getDimension(R.dimen.notification_large_icon_width);
-        int largeIconHeight = (int) context.getResources()
-                .getDimension(R.dimen.notification_large_icon_height);
-        Drawable d = context.getResources().getDrawable(resId);
-        Bitmap b = Bitmap.createBitmap(largeIconWidth, largeIconHeight, Bitmap.Config.ARGB_8888);
-        Canvas c = new Canvas(b);
-        d.setBounds(0, 0, largeIconWidth, largeIconHeight);
-        d.draw(c);
-        return b;
-    }
-
-    private static PendingIntent makeEmailIntent(Context context, String who) {
-        final Intent intent = new Intent(android.content.Intent.ACTION_SENDTO,
-                Uri.parse("mailto:" + who));
-        return PendingIntent.getActivity(
-                context, 0, intent,
-                PendingIntent.FLAG_CANCEL_CURRENT);
-    }
-
-    static final String[] LINES = new String[] {
-            "Uh oh",
-            "Getting kicked out of this room",
-            "I'll be back in 5-10 minutes.",
-            "And now \u2026 I have to find my shoes. \uD83D\uDC63",
-            "\uD83D\uDC5F \uD83D\uDC5F",
-            "\uD83D\uDC60 \uD83D\uDC60",
-    };
-    static final int MAX_LINES = 5;
-    public static Notification makeBigTextNotification(Context context, int update, int id,
-            long when) {
-        String personUri = null;
-        /*
-        Cursor c = null;
-        try {
-            String[] projection = new String[] { ContactsContract.Contacts._ID, ContactsContract.Contacts.LOOKUP_KEY };
-            String selections = ContactsContract.Contacts.DISPLAY_NAME + " = 'Mike Cleron'";
-            final ContentResolver contentResolver = context.getContentResolver();
-            c = contentResolver.query(ContactsContract.Contacts.CONTENT_URI,
-                    projection, selections, null, null);
-            if (c != null && c.getCount() > 0) {
-                c.moveToFirst();
-                int lookupIdx = c.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY);
-                int idIdx = c.getColumnIndex(ContactsContract.Contacts._ID);
-                String lookupKey = c.getString(lookupIdx);
-                long contactId = c.getLong(idIdx);
-                Uri lookupUri = ContactsContract.Contacts.getLookupUri(contactId, lookupKey);
-                personUri = lookupUri.toString();
-            }
-        } finally {
-            if (c != null) {
-                c.close();
-            }
-        }
-        if (TextUtils.isEmpty(personUri)) {
-            Log.w(TAG, "failed to find contact for Mike Cleron");
-        } else {
-            Log.w(TAG, "Mike Cleron is " + personUri);
-        }
-        */
-
-        StringBuilder longSmsText = new StringBuilder();
-        int end = 2 + update;
-        if (end > LINES.length) {
-            end = LINES.length;
-        }
-        final int start = Math.max(0, end - MAX_LINES);
-        for (int i=start; i<end; i++) {
-            if (i >= LINES.length) break;
-            if (i > start) longSmsText.append("\n");
-            longSmsText.append(LINES[i]);
-        }
-        if (update > 2) {
-            when = System.currentTimeMillis();
-        }
-        Notification.BigTextStyle bigTextStyle = new Notification.BigTextStyle()
-                .bigText(longSmsText);
-        Notification bigText = new Notification.Builder(context)
-                .setContentTitle("Mike Cleron")
-                .setContentIntent(ToastService.getPendingIntent(context, "Clicked on bigText"))
-                .setContentText(longSmsText)
-                        //.setTicker("Mike Cleron: " + longSmsText)
-                .setWhen(when)
-                .setLargeIcon(getBitmap(context, R.drawable.bucket))
-                .setPriority(Notification.PRIORITY_HIGH)
-                .setNumber(update)
-                .setSmallIcon(R.drawable.stat_notify_talk_text)
-                .setStyle(bigTextStyle)
-                .setDefaults(Notification.DEFAULT_SOUND)
-                .addPerson(personUri)
-                .build();
-        return bigText;
-    }
-
-    public static Notification makeUploadNotification(Context context, int progress, long when) {
-        Notification.Builder uploadNotification = new Notification.Builder(context)
-                .setContentTitle("File Upload")
-                .setContentText("foo.txt")
-                .setPriority(Notification.PRIORITY_MIN)
-                .setContentIntent(ToastService.getPendingIntent(context, "Clicked on Upload"))
-                .setWhen(when)
-                .setSmallIcon(R.drawable.ic_menu_upload)
-                .setProgress(100, Math.min(progress, 100), false);
-        return uploadNotification.build();
-    }
-
-    static SpannableStringBuilder BOLD(CharSequence str) {
-        final SpannableStringBuilder ssb = new SpannableStringBuilder(str);
-        ssb.setSpan(new StyleSpan(Typeface.BOLD), 0, ssb.length(), 0);
-        return ssb;
-    }
-
-    public static class ToastService extends IntentService {
-
-        private static final String TAG = "ToastService";
-
-        private static final String ACTION_TOAST = "toast";
-
-        private Handler handler;
-
-        public ToastService() {
-            super(TAG);
-        }
-        public ToastService(String name) {
-            super(name);
-        }
-
-        @Override
-        public int onStartCommand(Intent intent, int flags, int startId) {
-            handler = new Handler();
-            return super.onStartCommand(intent, flags, startId);
-        }
-
-        @Override
-        protected void onHandleIntent(Intent intent) {
-            Log.v(TAG, "clicked a thing! intent=" + intent.toString());
-            if (intent.hasExtra("text")) {
-                final String text = intent.getStringExtra("text");
-                handler.post(new Runnable() {
-                    @Override
-                    public void run() {
-                        Toast.makeText(ToastService.this, text, Toast.LENGTH_LONG).show();
-                        Log.v(TAG, "toast " + text);
-                    }
-                });
-            }
-        }
-
-        public static PendingIntent getPendingIntent(Context context, String text) {
-            Intent toastIntent = new Intent(context, ToastService.class);
-            toastIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-            toastIntent.setAction(ACTION_TOAST + ":" + text); // one per toast message
-            toastIntent.putExtra("text", text);
-            PendingIntent pi = PendingIntent.getService(
-                    context, 58, toastIntent, PendingIntent.FLAG_UPDATE_CURRENT);
-            return pi;
-        }
-    }
-
-    public static void sleepIfYouCan(int ms) {
-        try {
-            Thread.sleep(ms);
-        } catch (InterruptedException e) {}
-    }
-
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-    }
-
-    public static String summarize(Notification n) {
-        return String.format("<notif title=\"%s\" icon=0x%08x view=%s>",
-                n.extras.get(Notification.EXTRA_TITLE),
-                n.icon,
-                String.valueOf(n.contentView));
-    }
-    
-    public void testCreate() throws Exception {
-        ArrayList<Notification> mNotifications = new ArrayList<Notification>();
-        NotificationManager noMa =
-                (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
-
-        L("Constructing notifications...");
-        if (SHOW_BIG_TEXT) {
-            int bigtextId = mNotifications.size();
-            final long time = SystemClock.currentThreadTimeMillis();
-            final Notification n = makeBigTextNotification(mContext, 0, bigtextId, System.currentTimeMillis());
-            L("  %s: create=%dms", summarize(n), SystemClock.currentThreadTimeMillis() - time);
-            mNotifications.add(n);
-        }
-
-        int uploadId = mNotifications.size();
-        long uploadWhen = System.currentTimeMillis();
-
-        if (SHOW_PROGRESS) {
-            mNotifications.add(makeUploadNotification(mContext, 0, uploadWhen));
-        }
-
-        if (SHOW_PHONE_CALL) {
-            int phoneId = mNotifications.size();
-            final PendingIntent fullscreenIntent
-                    = FullScreenActivity.getPendingIntent(mContext, phoneId);
-            final long time = SystemClock.currentThreadTimeMillis();
-            Notification phoneCall = new Notification.Builder(mContext)
-                    .setContentTitle("Incoming call")
-                    .setContentText("Matias Duarte")
-                    .setLargeIcon(getBitmap(mContext, R.drawable.matias_hed))
-                    .setSmallIcon(R.drawable.stat_sys_phone_call)
-                    .setDefaults(Notification.DEFAULT_SOUND)
-                    .setPriority(Notification.PRIORITY_MAX)
-                    .setContentIntent(fullscreenIntent)
-                    .setFullScreenIntent(fullscreenIntent, true)
-                    .addAction(R.drawable.ic_dial_action_call, "Answer",
-                            ToastService.getPendingIntent(mContext, "Clicked on Answer"))
-                    .addAction(R.drawable.ic_end_call, "Ignore",
-                            ToastService.getPendingIntent(mContext, "Clicked on Ignore"))
-                    .setOngoing(true)
-                    .addPerson(Uri.fromParts("tel", "1 (617) 555-1212", null).toString())
-                    .build();
-            L("  %s: create=%dms", phoneCall.toString(), SystemClock.currentThreadTimeMillis() - time);
-            mNotifications.add(phoneCall);
-        }
-
-        if (SHOW_STOPWATCH) {
-            final long time = SystemClock.currentThreadTimeMillis();
-            final Notification n = new Notification.Builder(mContext)
-                    .setContentTitle("Stopwatch PRO")
-                    .setContentText("Counting up")
-                    .setContentIntent(ToastService.getPendingIntent(mContext, "Clicked on Stopwatch"))
-                    .setSmallIcon(R.drawable.stat_notify_alarm)
-                    .setUsesChronometer(true)
-                    .build();
-            L("  %s: create=%dms", summarize(n), SystemClock.currentThreadTimeMillis() - time);
-            mNotifications.add(n);
-        }
-
-        if (SHOW_CALENDAR) {
-            final long time = SystemClock.currentThreadTimeMillis();
-            final Notification n = new Notification.Builder(mContext)
-                    .setContentTitle("J Planning")
-                    .setContentText("The Botcave")
-                    .setWhen(System.currentTimeMillis())
-                    .setSmallIcon(R.drawable.stat_notify_calendar)
-                    .setContentIntent(ToastService.getPendingIntent(mContext, "Clicked on calendar event"))
-                    .setContentInfo("7PM")
-                    .addAction(R.drawable.stat_notify_snooze, "+10 min",
-                            ToastService.getPendingIntent(mContext, "snoozed 10 min"))
-                    .addAction(R.drawable.stat_notify_snooze_longer, "+1 hour",
-                            ToastService.getPendingIntent(mContext, "snoozed 1 hr"))
-                    .addAction(R.drawable.stat_notify_email, "Email",
-                            ToastService.getPendingIntent(mContext,
-                                    "Congratulations, you just destroyed someone's inbox zero"))
-                    .build();
-            L("  %s: create=%dms", summarize(n), SystemClock.currentThreadTimeMillis() - time);
-            mNotifications.add(n);
-        }
-
-        if (SHOW_BIG_PICTURE) {
-            BitmapDrawable d =
-                    (BitmapDrawable) mContext.getResources().getDrawable(R.drawable.romainguy_rockaway);
-            final long time = SystemClock.currentThreadTimeMillis();
-            final Notification n = new Notification.Builder(mContext)
-                        .setContentTitle("Romain Guy")
-                        .setContentText("I was lucky to find a Canon 5D Mk III at a local Bay Area "
-                                + "store last week but I had not been able to try it in the field "
-                                + "until tonight. After a few days of rain the sky finally cleared "
-                                + "up. Rockaway Beach did not disappoint and I was finally able to "
-                                + "see what my new camera feels like when shooting landscapes.")
-                        .setSmallIcon(android.R.drawable.stat_notify_chat)
-                        .setContentIntent(
-                                ToastService.getPendingIntent(mContext, "Clicked picture"))
-                        .setLargeIcon(getBitmap(mContext, R.drawable.romainguy_hed))
-                        .addAction(R.drawable.add, "Add to Gallery",
-                                ToastService.getPendingIntent(mContext, "Added"))
-                        .setStyle(new Notification.BigPictureStyle()
-                                .bigPicture(d.getBitmap()))
-                        .build();
-            L("  %s: create=%dms", summarize(n), SystemClock.currentThreadTimeMillis() - time);
-            mNotifications.add(n);
-        }
-
-        if (SHOW_INBOX) {
-            final long time = SystemClock.currentThreadTimeMillis();
-            final Notification n = new Notification.Builder(mContext)
-                    .setContentTitle("New mail")
-                    .setContentText("3 new messages")
-                    .setSubText("example@gmail.com")
-                    .setContentIntent(ToastService.getPendingIntent(mContext, "Clicked on Mail"))
-                    .setSmallIcon(R.drawable.stat_notify_email)
-                    .setStyle(new Notification.InboxStyle()
-                                    .setSummaryText("example@gmail.com")
-                                    .addLine(BOLD("Alice:").append(" hey there!"))
-                                    .addLine(BOLD("Bob:").append(" hi there!"))
-                                    .addLine(BOLD("Charlie:").append(" Iz IN UR EMAILZ!!"))
-                    ).build();
-            L("  %s: create=%dms", summarize(n), SystemClock.currentThreadTimeMillis() - time);
-            mNotifications.add(n);
-        }
-
-        if (SHOW_SOCIAL) {
-            final long time = SystemClock.currentThreadTimeMillis();
-            final Notification n = new Notification.Builder(mContext)
-                    .setContentTitle("Social Network")
-                    .setContentText("You were mentioned in a post")
-                    .setContentInfo("example@gmail.com")
-                    .setContentIntent(ToastService.getPendingIntent(mContext, "Clicked on Social"))
-                    .setSmallIcon(android.R.drawable.stat_notify_chat)
-                    .setPriority(Notification.PRIORITY_LOW)
-                    .build();
-            L("  %s: create=%dms", summarize(n), SystemClock.currentThreadTimeMillis() - time);
-            mNotifications.add(n);
-        }
-
-        L("Posting notifications...");
-        for (int i=0; i<mNotifications.size(); i++) {
-            final int count = 4;
-            for (int j=0; j<count; j++) {
-                long time = SystemClock.currentThreadTimeMillis();
-                final Notification n = mNotifications.get(i);
-                noMa.notify(NOTIFICATION_ID + i, n);
-                time = SystemClock.currentThreadTimeMillis() - time;
-                L("  %s: notify=%dms (%d/%d)", summarize(n), time,
-                        j + 1, count);
-                sleepIfYouCan(150);
-            }
-        }
-
-        sleepIfYouCan(1000);
-
-        L("Canceling notifications...");
-        for (int i=0; i<mNotifications.size(); i++) {
-            final Notification n = mNotifications.get(i);
-            long time = SystemClock.currentThreadTimeMillis();
-            noMa.cancel(NOTIFICATION_ID + i);
-            time = SystemClock.currentThreadTimeMillis() - time;
-            L("  %s: cancel=%dms", summarize(n), time);
-        }
-
-        sleepIfYouCan(500);
-
-        L("Parceling notifications...");
-        // we want to be able to use this test on older OSes that do not have getOpenAshmemSize
-        Method getOpenAshmemSize = null;
-        try {
-            getOpenAshmemSize = Parcel.class.getMethod("getOpenAshmemSize");
-        } catch (NoSuchMethodException ex) {
-        }
-        for (int i=0; i<mNotifications.size(); i++) {
-            Parcel p = Parcel.obtain();
-            {
-                final Notification n = mNotifications.get(i);
-                long time = SystemClock.currentThreadTimeMillis();
-                n.writeToParcel(p, 0);
-                time = SystemClock.currentThreadTimeMillis() - time;
-                L("  %s: write parcel=%dms size=%d ashmem=%s",
-                        summarize(n), time, p.dataPosition(),
-                        (getOpenAshmemSize != null)
-                            ? getOpenAshmemSize.invoke(p)
-                            : "???");
-                p.setDataPosition(0);
-            }
-
-            long time = SystemClock.currentThreadTimeMillis();
-            final Notification n2 = Notification.CREATOR.createFromParcel(p);
-            time = SystemClock.currentThreadTimeMillis() - time;
-            L("  %s: parcel read=%dms", summarize(n2), time);
-
-            time = SystemClock.currentThreadTimeMillis();
-            noMa.notify(NOTIFICATION_ID + i, n2);
-            time = SystemClock.currentThreadTimeMillis() - time;
-            L("  %s: notify=%dms", summarize(n2), time);
-        }
-
-        sleepIfYouCan(500);
-
-        L("Canceling notifications...");
-        for (int i=0; i<mNotifications.size(); i++) {
-            long time = SystemClock.currentThreadTimeMillis();
-            final Notification n = mNotifications.get(i);
-            noMa.cancel(NOTIFICATION_ID + i);
-            time = SystemClock.currentThreadTimeMillis() - time;
-            L("  %s: cancel=%dms", summarize(n), time);
-        }
-
-
-//            if (SHOW_PROGRESS) {
-//                ProgressService.startProgressUpdater(this, uploadId, uploadWhen, 0);
-//            }
-    }
-
-    public static class FullScreenActivity extends Activity {
-        public static final String EXTRA_ID = "id";
-
-        @Override
-        public void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            setContentView(R.layout.full_screen);
-            final Intent intent = getIntent();
-            if (intent != null && intent.hasExtra(EXTRA_ID)) {
-                final int id = intent.getIntExtra(EXTRA_ID, -1);
-                if (id >= 0) {
-                    NotificationManager noMa =
-                            (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
-                    noMa.cancel(NOTIFICATION_ID + id);
-                }
-            }
-        }
-
-        public void dismiss(View v) {
-            finish();
-        }
-
-        public static PendingIntent getPendingIntent(Context context, int id) {
-            Intent fullScreenIntent = new Intent(context, FullScreenActivity.class);
-            fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-
-            fullScreenIntent.putExtra(EXTRA_ID, id);
-            PendingIntent pi = PendingIntent.getActivity(
-                    context, 22, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT);
-            return pi;
-        }
-    }
-}
-
diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h
index 1b1e93bd..a08f385 100644
--- a/tools/aapt2/cmd/Link.h
+++ b/tools/aapt2/cmd/Link.h
@@ -323,6 +323,7 @@
             "should only be used together with the --static-lib flag.",
         &options_.merge_only);
     AddOptionalSwitch("-v", "Enables verbose logging.", &verbose_);
+    AddOptionalFlagList("--feature-flags", "Placeholder, to be implemented.", &feature_flags_args_);
   }
 
   int Action(const std::vector<std::string>& args) override;
@@ -347,6 +348,7 @@
   std::optional<std::string> stable_id_file_path_;
   std::vector<std::string> split_args_;
   std::optional<std::string> trace_folder_;
+  std::vector<std::string> feature_flags_args_;
 };
 
 }// namespace aapt